@skyhook-io/radar-app 0.1.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 +67 -0
- package/package.json +80 -0
- package/src/App.tsx +1538 -0
- package/src/RadarApp.tsx +145 -0
- package/src/api/apiResources.ts +28 -0
- package/src/api/client.ts +2583 -0
- package/src/api/config.ts +116 -0
- package/src/api/traffic.ts +139 -0
- package/src/components/ConnectionErrorView.tsx +272 -0
- package/src/components/ContextSwitcher.tsx +481 -0
- package/src/components/DebugOverlay.tsx +94 -0
- package/src/components/UserMenu.tsx +87 -0
- package/src/components/audit/AuditSettingsDialog.tsx +162 -0
- package/src/components/audit/AuditView.tsx +123 -0
- package/src/components/cost/CostTrendChart.tsx +388 -0
- package/src/components/cost/CostView.tsx +545 -0
- package/src/components/dock/BottomDock.tsx +96 -0
- package/src/components/dock/DockContext.tsx +11 -0
- package/src/components/dock/LocalTerminalTab.tsx +22 -0
- package/src/components/dock/LogsTab.tsx +26 -0
- package/src/components/dock/NodeTerminalTab.tsx +50 -0
- package/src/components/dock/TerminalTab.tsx +42 -0
- package/src/components/dock/TrafficFlowListTab.tsx +18 -0
- package/src/components/dock/WorkloadLogsTab.tsx +23 -0
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsActions.tsx +1 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +1 -0
- package/src/components/gitops/ManagedResourcesList.tsx +1 -0
- package/src/components/gitops/SyncCountdown.tsx +1 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/helm/ChartBrowser.tsx +580 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +774 -0
- package/src/components/helm/HelmView.tsx +475 -0
- package/src/components/helm/InstallWizard.tsx +1060 -0
- package/src/components/helm/ManifestDiffViewer.tsx +91 -0
- package/src/components/helm/ManifestViewer.tsx +61 -0
- package/src/components/helm/OwnedResources.tsx +465 -0
- package/src/components/helm/RevisionHistory.tsx +167 -0
- package/src/components/helm/ValuesDiffPreview.tsx +190 -0
- package/src/components/helm/ValuesViewer.tsx +365 -0
- package/src/components/helm/helm-utils.ts +37 -0
- package/src/components/home/ActivitySummary.tsx +262 -0
- package/src/components/home/CertificateHealthCard.tsx +105 -0
- package/src/components/home/ClusterHealthCard.tsx +483 -0
- package/src/components/home/CostCard.tsx +112 -0
- package/src/components/home/HealthRing.tsx +1 -0
- package/src/components/home/HelmSummary.tsx +129 -0
- package/src/components/home/HomeView.tsx +224 -0
- package/src/components/home/MCPSetupDialog.tsx +417 -0
- package/src/components/home/NetworkPolicyCoverageCard.tsx +109 -0
- package/src/components/home/TopologyPreview.tsx +219 -0
- package/src/components/home/TrafficSummary.tsx +154 -0
- package/src/components/logs/JsonLogLine.tsx +1 -0
- package/src/components/logs/LogCore.tsx +2 -0
- package/src/components/logs/LogsViewer.tsx +44 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +40 -0
- package/src/components/logs/useLogBuffer.ts +2 -0
- package/src/components/logs/useLogSearch.ts +1 -0
- package/src/components/portforward/PortForwardButton.tsx +375 -0
- package/src/components/portforward/PortForwardManager.tsx +871 -0
- package/src/components/resource/PrometheusCharts.tsx +687 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +214 -0
- package/src/components/resources/ImageFilesystemModal.tsx +745 -0
- package/src/components/resources/PodFilesystemModal.tsx +407 -0
- package/src/components/resources/ResourceDetailDrawer.tsx +43 -0
- package/src/components/resources/ResourcesView.tsx +190 -0
- package/src/components/resources/drawer-components.tsx +1 -0
- package/src/components/resources/file-browser-utils.ts +35 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +1 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +17 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +1 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +1 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/EventRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +1 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/JobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +44 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +1 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +1 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodRenderer.tsx +94 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +1 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +1 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +26 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/VPARenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +52 -0
- package/src/components/resources/renderers/argo-cells.tsx +1 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +1 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +1 -0
- package/src/components/resources/renderers/eso-cells.tsx +1 -0
- package/src/components/resources/renderers/flux-cells.tsx +1 -0
- package/src/components/resources/renderers/index.ts +91 -0
- package/src/components/resources/renderers/istio-cells.tsx +1 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +1 -0
- package/src/components/resources/renderers/keda-cells.tsx +1 -0
- package/src/components/resources/renderers/knative-cells.tsx +1 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +1 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +1 -0
- package/src/components/resources/renderers/traefik-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-shared.tsx +1 -0
- package/src/components/resources/renderers/velero-cells.tsx +1 -0
- package/src/components/resources/resource-utils-argo.ts +2 -0
- package/src/components/resources/resource-utils-certmanager.ts +2 -0
- package/src/components/resources/resource-utils-cnpg.ts +2 -0
- package/src/components/resources/resource-utils-eso.ts +2 -0
- package/src/components/resources/resource-utils-flux.ts +2 -0
- package/src/components/resources/resource-utils-istio.ts +2 -0
- package/src/components/resources/resource-utils-karpenter.ts +2 -0
- package/src/components/resources/resource-utils-keda.ts +2 -0
- package/src/components/resources/resource-utils-knative.ts +2 -0
- package/src/components/resources/resource-utils-kyverno.ts +2 -0
- package/src/components/resources/resource-utils-prometheus.ts +2 -0
- package/src/components/resources/resource-utils-traefik.ts +1 -0
- package/src/components/resources/resource-utils-trivy.ts +2 -0
- package/src/components/resources/resource-utils-velero.ts +2 -0
- package/src/components/resources/resource-utils.ts +5 -0
- package/src/components/settings/SettingsDialog.tsx +537 -0
- package/src/components/shared/CreateResourceDialog.tsx +17 -0
- package/src/components/shared/EditableYamlView.tsx +24 -0
- package/src/components/shared/LargeClusterNamespacePicker.tsx +70 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +31 -0
- package/src/components/timeline/DiffViewer.tsx +1 -0
- package/src/components/timeline/TimelineList.tsx +69 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1308 -0
- package/src/components/timeline/TimelineView.tsx +157 -0
- package/src/components/timeline/shared.tsx +1 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +571 -0
- package/src/components/traffic/TrafficFlowList.tsx +415 -0
- package/src/components/traffic/TrafficFlowListContext.tsx +68 -0
- package/src/components/traffic/TrafficGraph.tsx +1546 -0
- package/src/components/traffic/TrafficView.tsx +1213 -0
- package/src/components/traffic/TrafficWizard.tsx +386 -0
- package/src/components/traffic/index.ts +3 -0
- package/src/components/ui/CodeViewer.tsx +8 -0
- package/src/components/ui/CommandPalette.tsx +460 -0
- package/src/components/ui/ConfirmDialog.tsx +1 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +619 -0
- package/src/components/ui/ErrorBoundary.tsx +46 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -0
- package/src/components/ui/Markdown.tsx +108 -0
- package/src/components/ui/MetricsChart.tsx +1 -0
- package/src/components/ui/NamespaceSelector.tsx +436 -0
- package/src/components/ui/ResourceBar.tsx +1 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +301 -0
- package/src/components/ui/Toast.tsx +1 -0
- package/src/components/ui/Tooltip.tsx +1 -0
- package/src/components/ui/UpdateNotification.tsx +299 -0
- package/src/components/ui/YamlEditor.tsx +1 -0
- package/src/components/workload/WorkloadView.tsx +532 -0
- package/src/context/ConnectionContext.tsx +173 -0
- package/src/context/ContextSwitchContext.tsx +56 -0
- package/src/context/NavCustomization.tsx +62 -0
- package/src/context/ThemeContext.tsx +97 -0
- package/src/contexts/CapabilitiesContext.tsx +130 -0
- package/src/hooks/useAnimatedUnmount.ts +1 -0
- package/src/hooks/useDesktopDownload.ts +41 -0
- package/src/hooks/useEventSource.ts +262 -0
- package/src/hooks/useFavorites.ts +69 -0
- package/src/hooks/useKeyboardShortcuts.tsx +7 -0
- package/src/hooks/useRefreshAnimation.ts +1 -0
- package/src/index.css +243 -0
- package/src/index.ts +17 -0
- package/src/main.tsx +158 -0
- package/src/types/gitops.ts +2 -0
- package/src/types.ts +3 -0
- package/src/utils/animation.ts +2 -0
- package/src/utils/badge-colors.ts +2 -0
- package/src/utils/context-name.ts +2 -0
- package/src/utils/desktop-download.ts +66 -0
- package/src/utils/desktop-open-folder.ts +21 -0
- package/src/utils/format.ts +2 -0
- package/src/utils/log-format.ts +12 -0
- package/src/utils/navigation.ts +23 -0
- package/src/utils/resource-hierarchy.ts +2 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.ts +2 -0
- package/src/utils/traffic-colors.ts +54 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
import { Copy, Check, Settings, Pencil, X, Eye, Play, Loader2 } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import yaml from 'yaml'
|
|
5
|
+
import type { HelmValues, ValuesPreviewResponse } from '../../types'
|
|
6
|
+
import { CodeViewer } from '../ui/CodeViewer'
|
|
7
|
+
import { YamlEditor } from '../ui/YamlEditor'
|
|
8
|
+
import { useHelmPreviewValues, useHelmApplyValues } from '../../api/client'
|
|
9
|
+
import { useCanHelmWrite } from '../../contexts/CapabilitiesContext'
|
|
10
|
+
import { ValuesDiffPreview } from './ValuesDiffPreview'
|
|
11
|
+
|
|
12
|
+
interface ValuesViewerProps {
|
|
13
|
+
values?: HelmValues
|
|
14
|
+
isLoading: boolean
|
|
15
|
+
showAllValues: boolean
|
|
16
|
+
onToggleAllValues: (show: boolean) => void
|
|
17
|
+
onCopy: (text: string) => void
|
|
18
|
+
copied: boolean
|
|
19
|
+
// Required for editing
|
|
20
|
+
namespace?: string
|
|
21
|
+
name?: string
|
|
22
|
+
onApplySuccess?: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ValuesViewer({
|
|
26
|
+
values,
|
|
27
|
+
isLoading,
|
|
28
|
+
showAllValues,
|
|
29
|
+
onToggleAllValues,
|
|
30
|
+
onCopy,
|
|
31
|
+
copied,
|
|
32
|
+
namespace,
|
|
33
|
+
name,
|
|
34
|
+
onApplySuccess,
|
|
35
|
+
}: ValuesViewerProps) {
|
|
36
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
37
|
+
const [editedYaml, setEditedYaml] = useState('')
|
|
38
|
+
const [yamlError, setYamlError] = useState<string | null>(null)
|
|
39
|
+
const [previewData, setPreviewData] = useState<ValuesPreviewResponse | null>(null)
|
|
40
|
+
const [showPreview, setShowPreview] = useState(false)
|
|
41
|
+
|
|
42
|
+
const previewMutation = useHelmPreviewValues()
|
|
43
|
+
const applyMutation = useHelmApplyValues()
|
|
44
|
+
const canHelmWrite = useCanHelmWrite()
|
|
45
|
+
|
|
46
|
+
const canEdit = Boolean(namespace && name) && canHelmWrite
|
|
47
|
+
|
|
48
|
+
const displayValues = showAllValues && values?.computed ? values.computed : values?.userSupplied
|
|
49
|
+
const isEmpty = !displayValues || Object.keys(displayValues).length === 0
|
|
50
|
+
|
|
51
|
+
// Start editing mode
|
|
52
|
+
const handleStartEdit = useCallback(() => {
|
|
53
|
+
// Allow editing even with no user-supplied values (start with empty YAML)
|
|
54
|
+
const yamlStr = values?.userSupplied ? jsonToYaml(values.userSupplied) : ''
|
|
55
|
+
setEditedYaml(yamlStr)
|
|
56
|
+
setYamlError(null)
|
|
57
|
+
setIsEditing(true)
|
|
58
|
+
// Switch to user-supplied view when editing
|
|
59
|
+
if (showAllValues) {
|
|
60
|
+
onToggleAllValues(false)
|
|
61
|
+
}
|
|
62
|
+
}, [values?.userSupplied, showAllValues, onToggleAllValues])
|
|
63
|
+
|
|
64
|
+
// Cancel editing
|
|
65
|
+
const handleCancelEdit = useCallback(() => {
|
|
66
|
+
setIsEditing(false)
|
|
67
|
+
setEditedYaml('')
|
|
68
|
+
setYamlError(null)
|
|
69
|
+
setPreviewData(null)
|
|
70
|
+
setShowPreview(false)
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
// Parse YAML and validate
|
|
74
|
+
const parseYaml = useCallback((yamlStr: string): Record<string, unknown> | null => {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = yaml.parse(yamlStr)
|
|
77
|
+
setYamlError(null)
|
|
78
|
+
return parsed || {}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
setYamlError(err instanceof Error ? err.message : 'Invalid YAML')
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
}, [])
|
|
84
|
+
|
|
85
|
+
// Preview changes
|
|
86
|
+
const handlePreview = useCallback(async () => {
|
|
87
|
+
if (!namespace || !name) return
|
|
88
|
+
const parsed = parseYaml(editedYaml)
|
|
89
|
+
if (!parsed) return
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const result = await previewMutation.mutateAsync({
|
|
93
|
+
namespace,
|
|
94
|
+
name,
|
|
95
|
+
values: parsed,
|
|
96
|
+
})
|
|
97
|
+
setPreviewData(result)
|
|
98
|
+
setShowPreview(true)
|
|
99
|
+
} catch {
|
|
100
|
+
// Error is handled by mutation
|
|
101
|
+
}
|
|
102
|
+
}, [namespace, name, editedYaml, parseYaml, previewMutation])
|
|
103
|
+
|
|
104
|
+
// Apply changes
|
|
105
|
+
const handleApply = useCallback(async () => {
|
|
106
|
+
if (!namespace || !name) return
|
|
107
|
+
const parsed = parseYaml(editedYaml)
|
|
108
|
+
if (!parsed) return
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await applyMutation.mutateAsync({
|
|
112
|
+
namespace,
|
|
113
|
+
name,
|
|
114
|
+
values: parsed,
|
|
115
|
+
})
|
|
116
|
+
handleCancelEdit()
|
|
117
|
+
onApplySuccess?.()
|
|
118
|
+
} catch {
|
|
119
|
+
// Error is handled by mutation
|
|
120
|
+
}
|
|
121
|
+
}, [namespace, name, editedYaml, parseYaml, applyMutation, handleCancelEdit, onApplySuccess])
|
|
122
|
+
|
|
123
|
+
// Apply from preview modal
|
|
124
|
+
const handleApplyFromPreview = useCallback(async () => {
|
|
125
|
+
if (!previewData || !namespace || !name) return
|
|
126
|
+
try {
|
|
127
|
+
await applyMutation.mutateAsync({
|
|
128
|
+
namespace,
|
|
129
|
+
name,
|
|
130
|
+
values: previewData.newValues,
|
|
131
|
+
})
|
|
132
|
+
setShowPreview(false)
|
|
133
|
+
handleCancelEdit()
|
|
134
|
+
onApplySuccess?.()
|
|
135
|
+
} catch {
|
|
136
|
+
// Error is handled by mutation
|
|
137
|
+
}
|
|
138
|
+
}, [previewData, namespace, name, applyMutation, handleCancelEdit, onApplySuccess])
|
|
139
|
+
|
|
140
|
+
if (isLoading) {
|
|
141
|
+
return (
|
|
142
|
+
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">
|
|
143
|
+
Loading values...
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isEmpty && !isEditing) {
|
|
149
|
+
return (
|
|
150
|
+
<div className="p-4">
|
|
151
|
+
<div className="flex items-center justify-between mb-3">
|
|
152
|
+
<span className="text-sm font-medium text-theme-text-secondary">Values</span>
|
|
153
|
+
<div className="flex items-center gap-2">
|
|
154
|
+
<ToggleButton showAll={showAllValues} onToggle={onToggleAllValues} disabled={isEditing} />
|
|
155
|
+
{canEdit && (
|
|
156
|
+
<button
|
|
157
|
+
onClick={handleStartEdit}
|
|
158
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
159
|
+
>
|
|
160
|
+
<Pencil className="w-3.5 h-3.5" />
|
|
161
|
+
Edit
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
|
|
167
|
+
<Settings className="w-8 h-8 text-theme-text-disabled" />
|
|
168
|
+
<span>{showAllValues ? 'No computed values' : 'No user-supplied values'}</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const yamlContent = isEditing ? editedYaml : jsonToYaml(displayValues || {})
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className="p-4 flex flex-col h-full">
|
|
178
|
+
{/* Header */}
|
|
179
|
+
<div className="flex items-center justify-between mb-3">
|
|
180
|
+
<div className="flex items-center gap-2">
|
|
181
|
+
<span className="text-sm font-medium text-theme-text-secondary">
|
|
182
|
+
{isEditing ? 'Editing Values' : showAllValues ? 'All Values (Computed)' : 'User-Supplied Values'}
|
|
183
|
+
</span>
|
|
184
|
+
{isEditing && (
|
|
185
|
+
<span className="badge-sm bg-amber-500/20 text-amber-400 border-amber-500/30">
|
|
186
|
+
unsaved
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
<div className="flex items-center gap-2">
|
|
191
|
+
{!isEditing && (
|
|
192
|
+
<>
|
|
193
|
+
<ToggleButton showAll={showAllValues} onToggle={onToggleAllValues} disabled={isEditing} />
|
|
194
|
+
<button
|
|
195
|
+
onClick={() => onCopy(yamlContent)}
|
|
196
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
197
|
+
>
|
|
198
|
+
{copied ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
|
|
199
|
+
Copy
|
|
200
|
+
</button>
|
|
201
|
+
{canEdit && (
|
|
202
|
+
<button
|
|
203
|
+
onClick={handleStartEdit}
|
|
204
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 rounded border border-blue-500/30"
|
|
205
|
+
>
|
|
206
|
+
<Pencil className="w-3.5 h-3.5" />
|
|
207
|
+
Edit
|
|
208
|
+
</button>
|
|
209
|
+
)}
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
{isEditing && (
|
|
213
|
+
<>
|
|
214
|
+
<button
|
|
215
|
+
onClick={handleCancelEdit}
|
|
216
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
217
|
+
>
|
|
218
|
+
<X className="w-3.5 h-3.5" />
|
|
219
|
+
Cancel
|
|
220
|
+
</button>
|
|
221
|
+
<button
|
|
222
|
+
onClick={handlePreview}
|
|
223
|
+
disabled={!!yamlError || previewMutation.isPending}
|
|
224
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded border border-theme-border disabled:opacity-50 disabled:cursor-not-allowed"
|
|
225
|
+
>
|
|
226
|
+
{previewMutation.isPending ? (
|
|
227
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
228
|
+
) : (
|
|
229
|
+
<Eye className="w-3.5 h-3.5" />
|
|
230
|
+
)}
|
|
231
|
+
Preview
|
|
232
|
+
</button>
|
|
233
|
+
<button
|
|
234
|
+
onClick={handleApply}
|
|
235
|
+
disabled={!!yamlError || applyMutation.isPending || !canHelmWrite}
|
|
236
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs btn-brand rounded disabled:cursor-not-allowed"
|
|
237
|
+
title={!canHelmWrite ? 'Helm write permissions required (rbac.helm=true)' : undefined}
|
|
238
|
+
>
|
|
239
|
+
{applyMutation.isPending ? (
|
|
240
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
241
|
+
) : (
|
|
242
|
+
<Play className="w-3.5 h-3.5" />
|
|
243
|
+
)}
|
|
244
|
+
Apply
|
|
245
|
+
</button>
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* Error message */}
|
|
252
|
+
{yamlError && (
|
|
253
|
+
<div className="mb-3 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
|
|
254
|
+
{yamlError}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
{/* Mutation error */}
|
|
259
|
+
{(previewMutation.error || applyMutation.error) && (
|
|
260
|
+
<div className="mb-3 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
|
|
261
|
+
{previewMutation.error?.message || applyMutation.error?.message}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Editor / Viewer */}
|
|
266
|
+
<div className="flex-1 min-h-0">
|
|
267
|
+
{isEditing ? (
|
|
268
|
+
<YamlEditor
|
|
269
|
+
value={editedYaml}
|
|
270
|
+
onChange={setEditedYaml}
|
|
271
|
+
height="calc(100vh - 400px)"
|
|
272
|
+
onValidate={(isValid, errors) => {
|
|
273
|
+
setYamlError(isValid ? null : errors[0] || 'Invalid YAML')
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
) : (
|
|
277
|
+
<CodeViewer
|
|
278
|
+
code={yamlContent}
|
|
279
|
+
language="yaml"
|
|
280
|
+
showLineNumbers
|
|
281
|
+
maxHeight="calc(100vh - 300px)"
|
|
282
|
+
/>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{/* Preview Modal */}
|
|
287
|
+
{showPreview && previewData && (
|
|
288
|
+
<ValuesDiffPreview
|
|
289
|
+
previewData={previewData}
|
|
290
|
+
onClose={() => setShowPreview(false)}
|
|
291
|
+
onApply={handleApplyFromPreview}
|
|
292
|
+
isApplying={applyMutation.isPending}
|
|
293
|
+
/>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function ToggleButton({ showAll, onToggle, disabled }: { showAll: boolean; onToggle: (show: boolean) => void; disabled?: boolean }) {
|
|
300
|
+
return (
|
|
301
|
+
<div className={clsx('flex items-center bg-theme-elevated/50 rounded-md p-0.5 text-xs', disabled && 'opacity-50 pointer-events-none')}>
|
|
302
|
+
<button
|
|
303
|
+
onClick={() => onToggle(false)}
|
|
304
|
+
disabled={disabled}
|
|
305
|
+
className={clsx(
|
|
306
|
+
'px-2 py-1 rounded transition-colors',
|
|
307
|
+
!showAll ? 'bg-theme-hover text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary'
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
310
|
+
User
|
|
311
|
+
</button>
|
|
312
|
+
<button
|
|
313
|
+
onClick={() => onToggle(true)}
|
|
314
|
+
disabled={disabled}
|
|
315
|
+
className={clsx(
|
|
316
|
+
'px-2 py-1 rounded transition-colors',
|
|
317
|
+
showAll ? 'bg-theme-hover text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary'
|
|
318
|
+
)}
|
|
319
|
+
>
|
|
320
|
+
All
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Simple JSON to YAML converter for display
|
|
327
|
+
function jsonToYaml(obj: Record<string, unknown>, indent = 0): string {
|
|
328
|
+
const spaces = ' '.repeat(indent)
|
|
329
|
+
let result = ''
|
|
330
|
+
|
|
331
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
332
|
+
if (value === null || value === undefined) {
|
|
333
|
+
result += `${spaces}${key}: null\n`
|
|
334
|
+
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
335
|
+
result += `${spaces}${key}:\n`
|
|
336
|
+
result += jsonToYaml(value as Record<string, unknown>, indent + 1)
|
|
337
|
+
} else if (Array.isArray(value)) {
|
|
338
|
+
result += `${spaces}${key}:\n`
|
|
339
|
+
for (const item of value) {
|
|
340
|
+
if (typeof item === 'object' && item !== null) {
|
|
341
|
+
result += `${spaces}- \n`
|
|
342
|
+
const itemYaml = jsonToYaml(item as Record<string, unknown>, indent + 2)
|
|
343
|
+
result += itemYaml
|
|
344
|
+
} else {
|
|
345
|
+
result += `${spaces}- ${formatValue(item)}\n`
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
result += `${spaces}${key}: ${formatValue(value)}\n`
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatValue(value: unknown): string {
|
|
357
|
+
if (typeof value === 'string') {
|
|
358
|
+
// Quote strings that contain special characters
|
|
359
|
+
if (value.includes(':') || value.includes('#') || value.includes('\n') || value.startsWith(' ') || value.endsWith(' ')) {
|
|
360
|
+
return `"${value.replace(/"/g, '\\"')}"`
|
|
361
|
+
}
|
|
362
|
+
return value
|
|
363
|
+
}
|
|
364
|
+
return String(value)
|
|
365
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Utility functions for Helm components
|
|
2
|
+
|
|
3
|
+
import { getHelmStatusColor } from '../../utils/badge-colors'
|
|
4
|
+
// Re-export formatAge from resource-utils to avoid duplication
|
|
5
|
+
export { formatAge } from '../resources/resource-utils'
|
|
6
|
+
|
|
7
|
+
// Get status color classes for Helm release status
|
|
8
|
+
// Delegates to centralized badge-colors for consistency
|
|
9
|
+
export function getStatusColor(status: string): string {
|
|
10
|
+
return getHelmStatusColor(status)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Format date for display
|
|
14
|
+
export function formatDate(dateString: string): string {
|
|
15
|
+
const date = new Date(dateString)
|
|
16
|
+
return date.toLocaleString(undefined, {
|
|
17
|
+
year: 'numeric',
|
|
18
|
+
month: 'short',
|
|
19
|
+
day: 'numeric',
|
|
20
|
+
hour: '2-digit',
|
|
21
|
+
minute: '2-digit',
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Truncate text with ellipsis
|
|
26
|
+
export function truncate(text: string, maxLength: number): string {
|
|
27
|
+
if (text.length <= maxLength) return text
|
|
28
|
+
return text.slice(0, maxLength - 3) + '...'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get chart display name (combines chart name and version)
|
|
32
|
+
export function getChartDisplay(chart: string, version: string): string {
|
|
33
|
+
return `${chart}-${version}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Re-export kindToPlural from centralized navigation utils
|
|
37
|
+
export { kindToPlural } from '../../utils/navigation'
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Clock, ArrowRight, Shield } from 'lucide-react'
|
|
4
|
+
import { useChanges } from '../../api/client'
|
|
5
|
+
import { isChangeEvent } from '../../types'
|
|
6
|
+
import type { TimelineEvent } from '../../types'
|
|
7
|
+
import type { Topology } from '../../types'
|
|
8
|
+
import { useHasLimitedAccess } from '../../contexts/CapabilitiesContext'
|
|
9
|
+
import { buildResourceHierarchy, isProblematicEvent, type ResourceLane } from '../../utils/resource-hierarchy'
|
|
10
|
+
import { buildHealthSpans, timeToX } from '../timeline/shared'
|
|
11
|
+
|
|
12
|
+
interface ActivitySummaryProps {
|
|
13
|
+
namespaces: string[]
|
|
14
|
+
topology?: Topology | null
|
|
15
|
+
onNavigate: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_LANES = 6
|
|
19
|
+
const SPAN_MINUTES = 60
|
|
20
|
+
|
|
21
|
+
const HEALTH_SPAN_COLORS: Record<string, string> = {
|
|
22
|
+
healthy: 'bg-green-500/50 dark:bg-green-600/50',
|
|
23
|
+
rolling: 'bg-blue-500/50 dark:bg-blue-500/50',
|
|
24
|
+
degraded: 'bg-amber-500/50 dark:bg-[#b8861e]',
|
|
25
|
+
unhealthy: 'bg-red-500/50 dark:bg-red-500/50',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Simplified interestingness scoring for the mini view
|
|
29
|
+
function scoreLane(lane: ResourceLane): number {
|
|
30
|
+
const allEvents = [...lane.events, ...(lane.children?.flatMap(c => c.events) || [])]
|
|
31
|
+
let score = 0
|
|
32
|
+
|
|
33
|
+
const kindScores: Record<string, number> = {
|
|
34
|
+
Deployment: 50, Rollout: 50, StatefulSet: 50, DaemonSet: 50,
|
|
35
|
+
Service: 45, Ingress: 45, Gateway: 45,
|
|
36
|
+
HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42,
|
|
37
|
+
Job: 40, CronJob: 40,
|
|
38
|
+
Pod: 30, ReplicaSet: 20,
|
|
39
|
+
}
|
|
40
|
+
score += kindScores[lane.kind] || 15
|
|
41
|
+
|
|
42
|
+
// Problematic events are most important
|
|
43
|
+
score += allEvents.filter(e => isProblematicEvent(e)).length * 40
|
|
44
|
+
|
|
45
|
+
// Recency bonus
|
|
46
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1000
|
|
47
|
+
score += Math.min(allEvents.filter(e => new Date(e.timestamp).getTime() > fiveMinAgo).length * 30, 150)
|
|
48
|
+
|
|
49
|
+
// Children bonus
|
|
50
|
+
if (lane.children && lane.children.length > 0) score += 10
|
|
51
|
+
|
|
52
|
+
// System namespace penalty
|
|
53
|
+
if (['kube-system', 'kube-public', 'kube-node-lease'].includes(lane.namespace)) score -= 30
|
|
54
|
+
|
|
55
|
+
return score
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Short kind labels for compact display
|
|
59
|
+
const KIND_SHORT: Record<string, string> = {
|
|
60
|
+
Deployment: 'Deploy',
|
|
61
|
+
StatefulSet: 'SS',
|
|
62
|
+
DaemonSet: 'DS',
|
|
63
|
+
ReplicaSet: 'RS',
|
|
64
|
+
Service: 'Svc',
|
|
65
|
+
ConfigMap: 'CM',
|
|
66
|
+
CronJob: 'CJ',
|
|
67
|
+
Ingress: 'Ing',
|
|
68
|
+
Gateway: 'GW',
|
|
69
|
+
HTTPRoute: 'HR',
|
|
70
|
+
GRPCRoute: 'gRPC',
|
|
71
|
+
TCPRoute: 'TCP',
|
|
72
|
+
TLSRoute: 'TLS',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function ActivitySummary({ namespaces, topology, onNavigate }: ActivitySummaryProps) {
|
|
76
|
+
const hasLimitedAccess = useHasLimitedAccess()
|
|
77
|
+
const { data: events, isLoading, error } = useChanges({
|
|
78
|
+
namespaces,
|
|
79
|
+
timeRange: '1h',
|
|
80
|
+
includeK8sEvents: true,
|
|
81
|
+
includeManaged: true,
|
|
82
|
+
limit: 1000,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const now = useMemo(() => Date.now(), [events])
|
|
86
|
+
const spanMs = SPAN_MINUTES * 60 * 1000
|
|
87
|
+
const startTime = now - spanMs
|
|
88
|
+
|
|
89
|
+
const lanes = useMemo(() => {
|
|
90
|
+
if (!events || events.length === 0) return []
|
|
91
|
+
|
|
92
|
+
// Only use events within the visible window
|
|
93
|
+
const windowEvents = events.filter(e => {
|
|
94
|
+
const t = new Date(e.timestamp).getTime()
|
|
95
|
+
return t >= startTime && t <= now
|
|
96
|
+
})
|
|
97
|
+
if (windowEvents.length === 0) return []
|
|
98
|
+
|
|
99
|
+
const hierarchy = buildResourceHierarchy({
|
|
100
|
+
events: windowEvents,
|
|
101
|
+
topology: topology || undefined,
|
|
102
|
+
})
|
|
103
|
+
return hierarchy
|
|
104
|
+
.sort((a, b) => scoreLane(b) - scoreLane(a))
|
|
105
|
+
.slice(0, MAX_LANES)
|
|
106
|
+
}, [events, startTime, now, topology])
|
|
107
|
+
|
|
108
|
+
const hasActivity = lanes.length > 0
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<button
|
|
112
|
+
onClick={onNavigate}
|
|
113
|
+
className="group h-[260px] rounded-xl bg-theme-surface shadow-theme-sm hover:-translate-y-1 hover:shadow-theme-md transition-all duration-200 text-left"
|
|
114
|
+
>
|
|
115
|
+
<div className="flex flex-col h-full w-full">
|
|
116
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50">
|
|
117
|
+
<div className="flex items-center gap-2">
|
|
118
|
+
<Clock className="w-4 h-4 text-theme-text-tertiary" />
|
|
119
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-theme-text-secondary">Timeline</span>
|
|
120
|
+
</div>
|
|
121
|
+
<span className="text-xs text-theme-text-tertiary">last {SPAN_MINUTES}m</span>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Mini swimlanes */}
|
|
125
|
+
<div className="flex-1 min-h-0 overflow-hidden px-4 py-1.5">
|
|
126
|
+
{isLoading ? (
|
|
127
|
+
<div className="flex items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
|
|
128
|
+
Loading...
|
|
129
|
+
</div>
|
|
130
|
+
) : error ? (
|
|
131
|
+
<div className="flex items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
|
|
132
|
+
Could not load activity
|
|
133
|
+
</div>
|
|
134
|
+
) : !hasActivity ? (
|
|
135
|
+
<div className="flex flex-col items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
|
|
136
|
+
<span>No recent activity</span>
|
|
137
|
+
{hasLimitedAccess && (
|
|
138
|
+
<span className="flex items-center gap-1 mt-1.5 text-[11px] text-amber-400/80">
|
|
139
|
+
<Shield className="w-3 h-3" />
|
|
140
|
+
Some resource types are not monitored due to RBAC restrictions
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
) : (
|
|
145
|
+
<div className="space-y-1">
|
|
146
|
+
{lanes.map((lane) => (
|
|
147
|
+
<MiniLane
|
|
148
|
+
key={lane.id}
|
|
149
|
+
lane={lane}
|
|
150
|
+
startTime={startTime}
|
|
151
|
+
now={now}
|
|
152
|
+
spanMs={spanMs}
|
|
153
|
+
/>
|
|
154
|
+
))}
|
|
155
|
+
|
|
156
|
+
{/* Time axis */}
|
|
157
|
+
<div className="flex items-center justify-between text-[10px] text-theme-text-tertiary pt-1">
|
|
158
|
+
<span>{SPAN_MINUTES}m ago</span>
|
|
159
|
+
<span>now</span>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div className="px-4 py-1.5 border-t border-theme-border/50 flex items-center justify-end gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-theme-text-secondary group-hover:text-theme-text-primary transition-colors">
|
|
166
|
+
Open Timeline
|
|
167
|
+
<ArrowRight className="w-3.5 h-3.5 transition-transform group-hover:translate-x-0.5" />
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</button>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// A single compact swimlane row: [kind label + name] [health bar with event dots]
|
|
175
|
+
function MiniLane({ lane, startTime, now, spanMs }: {
|
|
176
|
+
lane: ResourceLane
|
|
177
|
+
startTime: number
|
|
178
|
+
now: number
|
|
179
|
+
spanMs: number
|
|
180
|
+
}) {
|
|
181
|
+
const allEvents: TimelineEvent[] = lane.allEventsSorted || [
|
|
182
|
+
...lane.events,
|
|
183
|
+
...(lane.children?.flatMap(c => c.events) || []),
|
|
184
|
+
]
|
|
185
|
+
const changeEvents = allEvents.filter(e => isChangeEvent(e))
|
|
186
|
+
const { spans } = buildHealthSpans(changeEvents, startTime, now, allEvents)
|
|
187
|
+
|
|
188
|
+
const hasProblems = allEvents.some(e => isProblematicEvent(e))
|
|
189
|
+
const kindLabel = KIND_SHORT[lane.kind] || lane.kind
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div className="flex items-center gap-2 h-5">
|
|
193
|
+
{/* Label */}
|
|
194
|
+
<div className="w-[6.5rem] shrink-0 flex items-center gap-1 min-w-0">
|
|
195
|
+
<span className={clsx(
|
|
196
|
+
'badge-sm shrink-0',
|
|
197
|
+
hasProblems
|
|
198
|
+
? 'status-degraded'
|
|
199
|
+
: 'bg-theme-elevated text-theme-text-tertiary',
|
|
200
|
+
)}>
|
|
201
|
+
{kindLabel}
|
|
202
|
+
</span>
|
|
203
|
+
<span className="text-[11px] text-theme-text-secondary truncate">
|
|
204
|
+
{lane.name}
|
|
205
|
+
</span>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* Health bar track */}
|
|
209
|
+
<div className="flex-1 relative h-3 bg-theme-border/20 rounded-sm overflow-hidden">
|
|
210
|
+
{/* Health state spans */}
|
|
211
|
+
{spans.map((span, i) => {
|
|
212
|
+
const left = timeToX(span.start, startTime, spanMs)
|
|
213
|
+
const right = timeToX(span.end, startTime, spanMs)
|
|
214
|
+
const width = right - left
|
|
215
|
+
if (width <= 0 || right < 0 || left > 100) return null
|
|
216
|
+
|
|
217
|
+
const clampedLeft = Math.max(0, left)
|
|
218
|
+
const clampedWidth = Math.min(100 - clampedLeft, width - (clampedLeft - left))
|
|
219
|
+
if (clampedWidth <= 0) return null
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div
|
|
223
|
+
key={i}
|
|
224
|
+
className={clsx(
|
|
225
|
+
'absolute top-0 bottom-0 rounded-sm',
|
|
226
|
+
HEALTH_SPAN_COLORS[span.health] || 'bg-gray-400/30',
|
|
227
|
+
)}
|
|
228
|
+
style={{ left: `${clampedLeft}%`, width: `${clampedWidth}%` }}
|
|
229
|
+
/>
|
|
230
|
+
)
|
|
231
|
+
})}
|
|
232
|
+
|
|
233
|
+
{/* All event dots — small for normal, larger for critical */}
|
|
234
|
+
{allEvents.map((event, idx) => {
|
|
235
|
+
const x = timeToX(new Date(event.timestamp).getTime(), startTime, spanMs)
|
|
236
|
+
if (x < 0 || x > 100) return null
|
|
237
|
+
|
|
238
|
+
const isCritical = isProblematicEvent(event)
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div
|
|
242
|
+
key={`${event.id}-${idx}`}
|
|
243
|
+
className={clsx(
|
|
244
|
+
'absolute top-1/2 -translate-y-1/2 -translate-x-1/2 rounded-full',
|
|
245
|
+
isCritical
|
|
246
|
+
? 'w-2.5 h-2.5 bg-red-500 ring-1 ring-red-500/30 z-10'
|
|
247
|
+
: 'w-1.5 h-1.5',
|
|
248
|
+
!isCritical && (
|
|
249
|
+
event.eventType === 'add' ? 'bg-green-500'
|
|
250
|
+
: event.eventType === 'delete' ? 'bg-red-500'
|
|
251
|
+
: event.eventType === 'update' ? 'bg-blue-500'
|
|
252
|
+
: 'bg-theme-text-tertiary'
|
|
253
|
+
),
|
|
254
|
+
)}
|
|
255
|
+
style={{ left: `${x}%` }}
|
|
256
|
+
/>
|
|
257
|
+
)
|
|
258
|
+
})}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
}
|