@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
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { History, Eye, GitCompare, Check, RotateCcw } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
-
import type { HelmRevision } from '../../types'
|
|
4
|
+
import type { HelmOperation, HelmRevision } from '../../types'
|
|
5
5
|
import { getStatusColor, formatDate, formatAge } from './helm-utils'
|
|
6
6
|
import { SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
8
|
|
|
8
9
|
interface RevisionHistoryProps {
|
|
9
10
|
history: HelmRevision[]
|
|
10
11
|
currentRevision: number
|
|
12
|
+
operations?: HelmOperation[]
|
|
11
13
|
onViewRevision: (revision: number) => void
|
|
12
14
|
onCompare: (rev1: number, rev2: number) => void
|
|
13
15
|
onRollback?: (revision: number) => void
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
export function RevisionHistory({ history, currentRevision, onViewRevision, onCompare, onRollback }: RevisionHistoryProps) {
|
|
18
|
+
export function RevisionHistory({ history, currentRevision, operations = [], onViewRevision, onCompare, onRollback }: RevisionHistoryProps) {
|
|
17
19
|
const [selectedForCompare, setSelectedForCompare] = useState<number | null>(null)
|
|
18
20
|
|
|
19
21
|
const handleCompareClick = (revision: number) => {
|
|
@@ -62,6 +64,7 @@ export function RevisionHistory({ history, currentRevision, onViewRevision, onCo
|
|
|
62
64
|
{history.map((revision, index) => {
|
|
63
65
|
const isCurrent = revision.revision === currentRevision
|
|
64
66
|
const isSelectedForCompare = selectedForCompare === revision.revision
|
|
67
|
+
const annotations = operationAnnotationsForRevision(operations, revision.revision, revision.status)
|
|
65
68
|
|
|
66
69
|
return (
|
|
67
70
|
<div
|
|
@@ -105,10 +108,15 @@ export function RevisionHistory({ history, currentRevision, onViewRevision, onCo
|
|
|
105
108
|
Current
|
|
106
109
|
</span>
|
|
107
110
|
)}
|
|
111
|
+
{annotations.map((annotation) => (
|
|
112
|
+
<span key={annotation.label} className={clsx('badge-sm', annotation.className)}>
|
|
113
|
+
{annotation.label}
|
|
114
|
+
</span>
|
|
115
|
+
))}
|
|
108
116
|
</div>
|
|
109
117
|
|
|
110
118
|
<div className="flex items-center gap-4 mt-1 text-xs text-theme-text-tertiary">
|
|
111
|
-
<
|
|
119
|
+
<Tooltip content={revision.updated}><span>{formatDate(revision.updated)}</span></Tooltip>
|
|
112
120
|
<span>{formatAge(revision.updated)} ago</span>
|
|
113
121
|
</div>
|
|
114
122
|
|
|
@@ -165,3 +173,44 @@ export function RevisionHistory({ history, currentRevision, onViewRevision, onCo
|
|
|
165
173
|
</div>
|
|
166
174
|
)
|
|
167
175
|
}
|
|
176
|
+
|
|
177
|
+
function operationAnnotationsForRevision(operations: HelmOperation[], revision: number, revisionStatus: string): Array<{ label: string; className: string }> {
|
|
178
|
+
const annotations: Array<{ label: string; className: string }> = []
|
|
179
|
+
const seen = new Set<string>()
|
|
180
|
+
const add = (label: string, className: string) => {
|
|
181
|
+
if (seen.has(label)) return
|
|
182
|
+
seen.add(label)
|
|
183
|
+
annotations.push({ label, className })
|
|
184
|
+
}
|
|
185
|
+
const addFailure = (label: string) => {
|
|
186
|
+
if (revisionStatus.toLowerCase() === 'failed') return
|
|
187
|
+
add(label, SEVERITY_BADGE.error)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const op of operations) {
|
|
191
|
+
if (op.failedRevision === revision) {
|
|
192
|
+
addFailure('Failed upgrade')
|
|
193
|
+
}
|
|
194
|
+
if (op.rollbackRevision === revision) {
|
|
195
|
+
add('Rollback revision', SEVERITY_BADGE.warning)
|
|
196
|
+
}
|
|
197
|
+
if (op.revision === revision) {
|
|
198
|
+
switch (op.kind) {
|
|
199
|
+
case 'upgrade_failed':
|
|
200
|
+
addFailure('Failed upgrade')
|
|
201
|
+
break
|
|
202
|
+
case 'release_failed':
|
|
203
|
+
addFailure('Failed')
|
|
204
|
+
break
|
|
205
|
+
case 'rollback':
|
|
206
|
+
add('Rollback', SEVERITY_BADGE.warning)
|
|
207
|
+
break
|
|
208
|
+
case 'pending':
|
|
209
|
+
add('Pending', SEVERITY_BADGE.warning)
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return annotations
|
|
216
|
+
}
|
|
@@ -22,7 +22,7 @@ interface RoleGatedPanelProps {
|
|
|
22
22
|
*
|
|
23
23
|
* Bypasses for non-Cloud users (OSS, OIDC, etc.) — `canAtLeast` returns
|
|
24
24
|
* true when no Cloud role is present. The backend gate has the same
|
|
25
|
-
* shape, so the
|
|
25
|
+
* shape, so the frontend stays in lockstep.
|
|
26
26
|
*/
|
|
27
27
|
export function RoleGatedPanel({ min, feature, children }: RoleGatedPanelProps) {
|
|
28
28
|
const { role, canAtLeast } = useCloudRole()
|
|
@@ -38,8 +38,8 @@ export function RoleGatedPanel({ min, feature, children }: RoleGatedPanelProps)
|
|
|
38
38
|
Your role can't view {feature}
|
|
39
39
|
</div>
|
|
40
40
|
<div className="mt-1 max-w-md text-xs text-theme-text-secondary">
|
|
41
|
-
You're signed in as <span className="
|
|
42
|
-
This view requires <span className="
|
|
41
|
+
You're signed in as <span className="inline-code">{role ?? 'viewer'}</span>.
|
|
42
|
+
This view requires <span className="inline-code">{min}</span> or higher.
|
|
43
43
|
Ask a {min} or owner if you need access.
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { DialogPortal } from '@skyhook-io/k8s-ui/components/ui/DialogPortal'
|
|
3
|
+
import { X, Plus, Trash2, Link2, AlertTriangle } from 'lucide-react'
|
|
4
|
+
import { clsx } from 'clsx'
|
|
5
|
+
import { useHelmOCISources, useAddOCISource, useRemoveOCISource, useClusterInfo } from '../../api/client'
|
|
6
|
+
import { Input } from '@skyhook-io/k8s-ui'
|
|
7
|
+
import type { UpgradeInfo } from '../../types'
|
|
8
|
+
|
|
9
|
+
interface TrackChartSourceDialogProps {
|
|
10
|
+
open: boolean
|
|
11
|
+
onClose: () => void
|
|
12
|
+
/** Chart name of the release this was opened from, for the example prompt. */
|
|
13
|
+
chartName?: string
|
|
14
|
+
sourceIssue?: UpgradeInfo['sourceIssue']
|
|
15
|
+
sourceError?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getSourceIssueCopy(sourceIssue: UpgradeInfo['sourceIssue'], sourceError?: string) {
|
|
19
|
+
switch (sourceIssue) {
|
|
20
|
+
case 'repo_index_error':
|
|
21
|
+
return {
|
|
22
|
+
title: 'A Helm repo index failed',
|
|
23
|
+
body: 'Fix, refresh, or remove the broken Helm repo index. If this release came from OCI, add its registry prefix and Radar will check OCI before reporting that repo error.',
|
|
24
|
+
}
|
|
25
|
+
case 'ambiguous_repository':
|
|
26
|
+
return {
|
|
27
|
+
title: 'Multiple Helm repos match',
|
|
28
|
+
body: 'Radar will not guess between classic repos. Adding an OCI prefix will not enable direct upgrades while those repos remain ambiguous.',
|
|
29
|
+
}
|
|
30
|
+
case 'untracked':
|
|
31
|
+
return {
|
|
32
|
+
title: 'Source not tracked',
|
|
33
|
+
body: 'Helm does not record the install source. Add the OCI prefix that contains this chart.',
|
|
34
|
+
}
|
|
35
|
+
default:
|
|
36
|
+
return sourceError
|
|
37
|
+
? { title: 'Source unresolved', body: sourceError }
|
|
38
|
+
: undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// TrackChartSourceDialog lets the user register an OCI chart-source prefix — the
|
|
43
|
+
// OCI analog of `helm repo add`. Helm doesn't persist the ref a release was
|
|
44
|
+
// installed from, so for charts published to an OCI registry (and not managed by
|
|
45
|
+
// GitOps) Radar can only track upgrades once the user declares where they live.
|
|
46
|
+
// Registering a registry/org prefix lets Radar probe "<prefix>/<chartName>".
|
|
47
|
+
export function TrackChartSourceDialog({ open, onClose, chartName, sourceIssue, sourceError }: TrackChartSourceDialogProps) {
|
|
48
|
+
const [value, setValue] = useState('')
|
|
49
|
+
const { data: sources } = useHelmOCISources()
|
|
50
|
+
const { data: clusterInfo } = useClusterInfo()
|
|
51
|
+
const addSource = useAddOCISource()
|
|
52
|
+
const removeSource = useRemoveOCISource()
|
|
53
|
+
|
|
54
|
+
// In-cluster Radar has no `helm registry login` store (the pod's HELM_CONFIG_HOME
|
|
55
|
+
// points at an empty /tmp), so private registries can't authenticate — only
|
|
56
|
+
// public charts can be tracked. Be honest about it rather than silently failing.
|
|
57
|
+
const inCluster = clusterInfo?.inCluster ?? false
|
|
58
|
+
|
|
59
|
+
const trimmed = value.trim()
|
|
60
|
+
const invalid = trimmed !== '' && !trimmed.startsWith('oci://')
|
|
61
|
+
const normalizedInput = trimmed.replace(/\/+$/, '')
|
|
62
|
+
const normalizedChartName = chartName?.trim().replace(/^\/+|\/+$/g, '') ?? ''
|
|
63
|
+
const probedRef = normalizedInput && normalizedChartName ? `${normalizedInput}/${normalizedChartName}` : ''
|
|
64
|
+
const looksLikeFullChartRef = Boolean(normalizedInput && normalizedChartName && normalizedInput.endsWith(`/${normalizedChartName}`))
|
|
65
|
+
const sourceIssueCopy = getSourceIssueCopy(sourceIssue, sourceError)
|
|
66
|
+
|
|
67
|
+
const handleAdd = () => {
|
|
68
|
+
if (!trimmed || invalid) return
|
|
69
|
+
addSource.mutate(trimmed, { onSuccess: () => setValue('') })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<DialogPortal open={open} onClose={onClose} className="max-w-lg w-full">
|
|
74
|
+
<div className="flex items-start gap-3 p-4 border-b border-theme-border">
|
|
75
|
+
<div className="flex items-center justify-center w-10 h-10 rounded-full shrink-0 bg-theme-hover">
|
|
76
|
+
<Link2 className="w-5 h-5 text-theme-text-secondary" />
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex-1 min-w-0">
|
|
79
|
+
<h3 className="text-lg font-semibold text-theme-text-primary">Track chart source</h3>
|
|
80
|
+
<p className="text-sm text-theme-text-secondary mt-1">
|
|
81
|
+
Helm doesn't record where a chart was installed from. Register your OCI
|
|
82
|
+
registry prefix and Radar will check it for newer versions of your charts.
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
<button
|
|
86
|
+
onClick={onClose}
|
|
87
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
88
|
+
>
|
|
89
|
+
<X className="w-5 h-5" />
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="p-4 space-y-4">
|
|
94
|
+
{sourceIssueCopy && (
|
|
95
|
+
<div className="flex items-start gap-2 rounded-lg border border-theme-border bg-theme-elevated px-3 py-2 text-sm">
|
|
96
|
+
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-theme-text-secondary" />
|
|
97
|
+
<div>
|
|
98
|
+
<p className="font-medium text-theme-text-primary">{sourceIssueCopy.title}</p>
|
|
99
|
+
<p className="text-theme-text-secondary">{sourceIssueCopy.body}</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<div>
|
|
105
|
+
<label className="block text-sm font-medium text-theme-text-secondary mb-2">
|
|
106
|
+
OCI registry prefix
|
|
107
|
+
</label>
|
|
108
|
+
<div className="flex gap-2">
|
|
109
|
+
<Input
|
|
110
|
+
value={value}
|
|
111
|
+
onChange={(e) => setValue(e.target.value)}
|
|
112
|
+
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
|
113
|
+
placeholder="oci://ghcr.io/myorg/charts"
|
|
114
|
+
aria-invalid={invalid ? true : undefined}
|
|
115
|
+
className={clsx(
|
|
116
|
+
'flex-1 px-3 py-2 bg-theme-elevated border rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2',
|
|
117
|
+
invalid ? 'border-red-500/60 focus:ring-red-500' : 'border-theme-border-light focus:ring-accent',
|
|
118
|
+
)}
|
|
119
|
+
/>
|
|
120
|
+
<button
|
|
121
|
+
onClick={handleAdd}
|
|
122
|
+
disabled={!trimmed || invalid || addSource.isPending}
|
|
123
|
+
className="btn-brand px-3 py-2 text-sm inline-flex items-center gap-1 disabled:opacity-50 disabled:pointer-events-none"
|
|
124
|
+
>
|
|
125
|
+
<Plus className="w-4 h-4" />
|
|
126
|
+
Add
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
130
|
+
{invalid
|
|
131
|
+
? 'Must be an oci:// reference.'
|
|
132
|
+
: looksLikeFullChartRef
|
|
133
|
+
? `This looks like a full chart ref. Radar would probe "${probedRef}"; remove the final "/${normalizedChartName}" if "${normalizedChartName}" is the chart name.`
|
|
134
|
+
: chartName
|
|
135
|
+
? `Radar will probe "${probedRef || `<prefix>/${normalizedChartName}`}".`
|
|
136
|
+
: 'Radar probes <prefix>/<chartName> for each untracked release.'}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{sources && sources.length > 0 && (
|
|
141
|
+
<div>
|
|
142
|
+
<p className="text-xs font-medium text-theme-text-tertiary uppercase tracking-wide mb-2">
|
|
143
|
+
Registered sources
|
|
144
|
+
</p>
|
|
145
|
+
<ul className="space-y-1">
|
|
146
|
+
{sources.map((src) => (
|
|
147
|
+
<li
|
|
148
|
+
key={src}
|
|
149
|
+
className="flex items-center justify-between gap-2 px-3 py-2 bg-theme-elevated rounded-lg"
|
|
150
|
+
>
|
|
151
|
+
<span className="text-sm text-theme-text-primary font-mono truncate">{src}</span>
|
|
152
|
+
<button
|
|
153
|
+
onClick={() => removeSource.mutate(src)}
|
|
154
|
+
disabled={removeSource.isPending}
|
|
155
|
+
className="p-1 text-theme-text-secondary hover:text-red-400 hover:bg-red-500/10 rounded disabled:opacity-50"
|
|
156
|
+
aria-label={`Remove ${src}`}
|
|
157
|
+
>
|
|
158
|
+
<Trash2 className="w-4 h-4" />
|
|
159
|
+
</button>
|
|
160
|
+
</li>
|
|
161
|
+
))}
|
|
162
|
+
</ul>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{inCluster ? (
|
|
167
|
+
<div className="flex items-start gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-400">
|
|
168
|
+
<AlertTriangle className="w-4 h-4 shrink-0 mt-px" />
|
|
169
|
+
<span>
|
|
170
|
+
Radar is running in-cluster, where it has no{' '}
|
|
171
|
+
<span className="font-mono">helm registry login</span> credentials — only{' '}
|
|
172
|
+
<strong>public</strong> charts can be tracked. Private-registry support for in-cluster
|
|
173
|
+
Radar isn't available yet.
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
178
|
+
Credentials are reused from your <span className="font-mono">helm registry login</span>.
|
|
179
|
+
Radar stores no registry secrets.
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</DialogPortal>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { X, Play, Loader2, FileText, AlertTriangle } from 'lucide-react'
|
|
3
4
|
import { clsx } from 'clsx'
|
|
5
|
+
import { classifyDiffLine } from '@skyhook-io/k8s-ui'
|
|
4
6
|
import type { ValuesPreviewResponse } from '../../types'
|
|
5
7
|
|
|
6
8
|
interface ValuesDiffPreviewProps {
|
|
@@ -8,6 +10,8 @@ interface ValuesDiffPreviewProps {
|
|
|
8
10
|
onClose: () => void
|
|
9
11
|
onApply: () => void
|
|
10
12
|
isApplying: boolean
|
|
13
|
+
title?: string
|
|
14
|
+
applyLabel?: string
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export function ValuesDiffPreview({
|
|
@@ -15,6 +19,8 @@ export function ValuesDiffPreview({
|
|
|
15
19
|
onClose,
|
|
16
20
|
onApply,
|
|
17
21
|
isApplying,
|
|
22
|
+
title = 'Preview Changes',
|
|
23
|
+
applyLabel = 'Apply Changes',
|
|
18
24
|
}: ValuesDiffPreviewProps) {
|
|
19
25
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
20
26
|
|
|
@@ -40,7 +46,10 @@ export function ValuesDiffPreview({
|
|
|
40
46
|
const hasChanges = previewData.manifestDiff.trim().length > 0 &&
|
|
41
47
|
previewData.manifestDiff.split('\n').some(line => line.startsWith('+') || line.startsWith('-'))
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
// Portal to body so the modal escapes any CSS-transformed ancestor (the release
|
|
50
|
+
// drawer, the upgrade ConfirmDialog) and layers on top rather than being trapped
|
|
51
|
+
// in a lower stacking context.
|
|
52
|
+
return createPortal(
|
|
44
53
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
45
54
|
{/* Backdrop */}
|
|
46
55
|
<div
|
|
@@ -51,6 +60,8 @@ export function ValuesDiffPreview({
|
|
|
51
60
|
{/* Dialog */}
|
|
52
61
|
<div
|
|
53
62
|
ref={dialogRef}
|
|
63
|
+
role="dialog"
|
|
64
|
+
aria-modal="true"
|
|
54
65
|
tabIndex={-1}
|
|
55
66
|
className="relative dialog max-w-4xl w-full mx-4 max-h-[85vh] flex flex-col outline-none"
|
|
56
67
|
>
|
|
@@ -58,7 +69,7 @@ export function ValuesDiffPreview({
|
|
|
58
69
|
<div className="flex items-center justify-between px-4 py-3 border-b border-theme-border shrink-0">
|
|
59
70
|
<div className="flex items-center gap-2">
|
|
60
71
|
<FileText className="w-5 h-5 text-blue-400" />
|
|
61
|
-
<h3 className="text-lg font-semibold text-theme-text-primary">
|
|
72
|
+
<h3 className="text-lg font-semibold text-theme-text-primary">{title}</h3>
|
|
62
73
|
</div>
|
|
63
74
|
<button
|
|
64
75
|
onClick={onClose}
|
|
@@ -120,12 +131,13 @@ export function ValuesDiffPreview({
|
|
|
120
131
|
) : (
|
|
121
132
|
<Play className="w-4 h-4" />
|
|
122
133
|
)}
|
|
123
|
-
|
|
134
|
+
{applyLabel}
|
|
124
135
|
</button>
|
|
125
136
|
</div>
|
|
126
137
|
</div>
|
|
127
138
|
</div>
|
|
128
|
-
</div
|
|
139
|
+
</div>,
|
|
140
|
+
document.body
|
|
129
141
|
)
|
|
130
142
|
}
|
|
131
143
|
|
|
@@ -158,9 +170,7 @@ interface DiffLineProps {
|
|
|
158
170
|
}
|
|
159
171
|
|
|
160
172
|
function DiffLine({ line, lineNumber }: DiffLineProps) {
|
|
161
|
-
const isAddition =
|
|
162
|
-
const isDeletion = line.startsWith('-') && !line.startsWith('---')
|
|
163
|
-
const isHeader = line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')
|
|
173
|
+
const { isAddition, isRemoval: isDeletion, isHeader } = classifyDiffLine(line)
|
|
164
174
|
|
|
165
175
|
return (
|
|
166
176
|
<div
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react'
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
2
2
|
import { Copy, Check, Settings, Pencil, X, Eye, Play, Loader2 } from 'lucide-react'
|
|
3
3
|
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
@@ -9,6 +9,7 @@ import { YamlEditor } from '../ui/YamlEditor'
|
|
|
9
9
|
import { useHelmPreviewValues, useHelmApplyValues } from '../../api/client'
|
|
10
10
|
import { useCanHelmAct } from '../../api/client'
|
|
11
11
|
import { ValuesDiffPreview } from './ValuesDiffPreview'
|
|
12
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
12
13
|
|
|
13
14
|
interface ValuesViewerProps {
|
|
14
15
|
values?: HelmValues
|
|
@@ -20,9 +21,10 @@ interface ValuesViewerProps {
|
|
|
20
21
|
// Required for editing
|
|
21
22
|
namespace?: string
|
|
22
23
|
name?: string
|
|
24
|
+
revision?: number
|
|
25
|
+
currentRevision?: number
|
|
23
26
|
onApplySuccess?: () => void
|
|
24
27
|
}
|
|
25
|
-
|
|
26
28
|
export function ValuesViewer({
|
|
27
29
|
values,
|
|
28
30
|
isLoading,
|
|
@@ -32,6 +34,8 @@ export function ValuesViewer({
|
|
|
32
34
|
copied,
|
|
33
35
|
namespace,
|
|
34
36
|
name,
|
|
37
|
+
revision,
|
|
38
|
+
currentRevision,
|
|
35
39
|
onApplySuccess,
|
|
36
40
|
}: ValuesViewerProps) {
|
|
37
41
|
const [isEditing, setIsEditing] = useState(false)
|
|
@@ -43,8 +47,9 @@ export function ValuesViewer({
|
|
|
43
47
|
const previewMutation = useHelmPreviewValues()
|
|
44
48
|
const applyMutation = useHelmApplyValues()
|
|
45
49
|
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
|
|
50
|
+
const isHistoricalRevision = typeof revision === 'number' && typeof currentRevision === 'number' && revision !== currentRevision
|
|
46
51
|
|
|
47
|
-
const canEdit = Boolean(namespace && name) && canHelmWrite
|
|
52
|
+
const canEdit = Boolean(namespace && name) && canHelmWrite && !isHistoricalRevision
|
|
48
53
|
|
|
49
54
|
const displayValues = showAllValues && values?.computed ? values.computed : values?.userSupplied
|
|
50
55
|
const isEmpty = !displayValues || Object.keys(displayValues).length === 0
|
|
@@ -71,6 +76,12 @@ export function ValuesViewer({
|
|
|
71
76
|
setShowPreview(false)
|
|
72
77
|
}, [])
|
|
73
78
|
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (isHistoricalRevision && isEditing) {
|
|
81
|
+
handleCancelEdit()
|
|
82
|
+
}
|
|
83
|
+
}, [isHistoricalRevision, isEditing, handleCancelEdit])
|
|
84
|
+
|
|
74
85
|
// Parse YAML and validate
|
|
75
86
|
const parseYaml = useCallback((yamlStr: string): Record<string, unknown> | null => {
|
|
76
87
|
try {
|
|
@@ -85,7 +96,7 @@ export function ValuesViewer({
|
|
|
85
96
|
|
|
86
97
|
// Preview changes
|
|
87
98
|
const handlePreview = useCallback(async () => {
|
|
88
|
-
if (!namespace || !name) return
|
|
99
|
+
if (!namespace || !name || isHistoricalRevision) return
|
|
89
100
|
const parsed = parseYaml(editedYaml)
|
|
90
101
|
if (!parsed) return
|
|
91
102
|
|
|
@@ -100,11 +111,11 @@ export function ValuesViewer({
|
|
|
100
111
|
} catch {
|
|
101
112
|
// Error is handled by mutation
|
|
102
113
|
}
|
|
103
|
-
}, [namespace, name, editedYaml, parseYaml, previewMutation])
|
|
114
|
+
}, [namespace, name, isHistoricalRevision, editedYaml, parseYaml, previewMutation])
|
|
104
115
|
|
|
105
116
|
// Apply changes
|
|
106
117
|
const handleApply = useCallback(async () => {
|
|
107
|
-
if (!namespace || !name) return
|
|
118
|
+
if (!namespace || !name || isHistoricalRevision) return
|
|
108
119
|
const parsed = parseYaml(editedYaml)
|
|
109
120
|
if (!parsed) return
|
|
110
121
|
|
|
@@ -119,11 +130,11 @@ export function ValuesViewer({
|
|
|
119
130
|
} catch {
|
|
120
131
|
// Error is handled by mutation
|
|
121
132
|
}
|
|
122
|
-
}, [namespace, name, editedYaml, parseYaml, applyMutation, handleCancelEdit, onApplySuccess])
|
|
133
|
+
}, [namespace, name, isHistoricalRevision, editedYaml, parseYaml, applyMutation, handleCancelEdit, onApplySuccess])
|
|
123
134
|
|
|
124
135
|
// Apply from preview modal
|
|
125
136
|
const handleApplyFromPreview = useCallback(async () => {
|
|
126
|
-
if (!previewData || !namespace || !name) return
|
|
137
|
+
if (!previewData || !namespace || !name || isHistoricalRevision) return
|
|
127
138
|
try {
|
|
128
139
|
await applyMutation.mutateAsync({
|
|
129
140
|
namespace,
|
|
@@ -136,7 +147,7 @@ export function ValuesViewer({
|
|
|
136
147
|
} catch {
|
|
137
148
|
// Error is handled by mutation
|
|
138
149
|
}
|
|
139
|
-
}, [previewData, namespace, name, applyMutation, handleCancelEdit, onApplySuccess])
|
|
150
|
+
}, [previewData, namespace, name, isHistoricalRevision, applyMutation, handleCancelEdit, onApplySuccess])
|
|
140
151
|
|
|
141
152
|
if (isLoading) {
|
|
142
153
|
return <PaneLoader label="Loading values…" className="h-32" />
|
|
@@ -146,7 +157,12 @@ export function ValuesViewer({
|
|
|
146
157
|
return (
|
|
147
158
|
<div className="p-4">
|
|
148
159
|
<div className="flex items-center justify-between mb-3">
|
|
149
|
-
<
|
|
160
|
+
<div className="flex items-center gap-2">
|
|
161
|
+
<span className="text-sm font-medium text-theme-text-secondary">Values</span>
|
|
162
|
+
{isHistoricalRevision && (
|
|
163
|
+
<span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">revision {revision}</span>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
150
166
|
<div className="flex items-center gap-2">
|
|
151
167
|
<ToggleButton showAll={showAllValues} onToggle={onToggleAllValues} disabled={isEditing} />
|
|
152
168
|
{canEdit && (
|
|
@@ -160,6 +176,11 @@ export function ValuesViewer({
|
|
|
160
176
|
)}
|
|
161
177
|
</div>
|
|
162
178
|
</div>
|
|
179
|
+
{isHistoricalRevision && (
|
|
180
|
+
<div className="mb-3 rounded border border-theme-border bg-theme-elevated/40 px-3 py-2 text-xs text-theme-text-secondary">
|
|
181
|
+
Viewing historical values. Switch back to the latest revision before editing or applying changes.
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
163
184
|
<div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
|
|
164
185
|
<Settings className="w-8 h-8 text-theme-text-disabled" />
|
|
165
186
|
<span>{showAllValues ? 'No computed values' : 'No user-supplied values'}</span>
|
|
@@ -178,6 +199,9 @@ export function ValuesViewer({
|
|
|
178
199
|
<span className="text-sm font-medium text-theme-text-secondary">
|
|
179
200
|
{isEditing ? 'Editing Values' : showAllValues ? 'All Values (Computed)' : 'User-Supplied Values'}
|
|
180
201
|
</span>
|
|
202
|
+
{isHistoricalRevision && !isEditing && (
|
|
203
|
+
<span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">revision {revision}</span>
|
|
204
|
+
)}
|
|
181
205
|
{isEditing && (
|
|
182
206
|
<span className="badge-sm bg-amber-500/20 text-amber-400 border-amber-500/30">
|
|
183
207
|
unsaved
|
|
@@ -217,7 +241,7 @@ export function ValuesViewer({
|
|
|
217
241
|
</button>
|
|
218
242
|
<button
|
|
219
243
|
onClick={handlePreview}
|
|
220
|
-
disabled={!!yamlError || previewMutation.isPending}
|
|
244
|
+
disabled={!!yamlError || previewMutation.isPending || isHistoricalRevision}
|
|
221
245
|
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"
|
|
222
246
|
>
|
|
223
247
|
{previewMutation.isPending ? (
|
|
@@ -227,11 +251,11 @@ export function ValuesViewer({
|
|
|
227
251
|
)}
|
|
228
252
|
Preview
|
|
229
253
|
</button>
|
|
254
|
+
<Tooltip content={!canHelmWrite ? helmActReason : ''}>
|
|
230
255
|
<button
|
|
231
256
|
onClick={handleApply}
|
|
232
|
-
disabled={!!yamlError || applyMutation.isPending || !canHelmWrite}
|
|
233
|
-
className="flex items-center gap-1 px-2.5 py-1 text-xs btn-brand rounded disabled:cursor-not-allowed"
|
|
234
|
-
title={!canHelmWrite ? helmActReason : undefined}
|
|
257
|
+
disabled={!!yamlError || applyMutation.isPending || !canHelmWrite || isHistoricalRevision}
|
|
258
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs btn-brand rounded disabled:cursor-not-allowed disabled:pointer-events-none"
|
|
235
259
|
>
|
|
236
260
|
{applyMutation.isPending ? (
|
|
237
261
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
@@ -240,11 +264,18 @@ export function ValuesViewer({
|
|
|
240
264
|
)}
|
|
241
265
|
Apply
|
|
242
266
|
</button>
|
|
267
|
+
</Tooltip>
|
|
243
268
|
</>
|
|
244
269
|
)}
|
|
245
270
|
</div>
|
|
246
271
|
</div>
|
|
247
272
|
|
|
273
|
+
{isHistoricalRevision && (
|
|
274
|
+
<div className="mb-3 rounded border border-theme-border bg-theme-elevated/40 px-3 py-2 text-xs text-theme-text-secondary">
|
|
275
|
+
Viewing historical values. Switch back to the latest revision before editing or applying changes.
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
|
|
248
279
|
{/* Error message */}
|
|
249
280
|
{yamlError && (
|
|
250
281
|
<div className="mb-3 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
|
|
@@ -265,6 +296,7 @@ export function ValuesViewer({
|
|
|
265
296
|
<YamlEditor
|
|
266
297
|
value={editedYaml}
|
|
267
298
|
onChange={setEditedYaml}
|
|
299
|
+
showProblems={false}
|
|
268
300
|
height="calc(100vh - 400px)"
|
|
269
301
|
onValidate={(isValid, errors) => {
|
|
270
302
|
setYamlError(isValid ? null : errors[0] || 'Invalid YAML')
|
|
@@ -320,43 +352,7 @@ function ToggleButton({ showAll, onToggle, disabled }: { showAll: boolean; onTog
|
|
|
320
352
|
)
|
|
321
353
|
}
|
|
322
354
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
let result = ''
|
|
327
|
-
|
|
328
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
329
|
-
if (value === null || value === undefined) {
|
|
330
|
-
result += `${spaces}${key}: null\n`
|
|
331
|
-
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
332
|
-
result += `${spaces}${key}:\n`
|
|
333
|
-
result += jsonToYaml(value as Record<string, unknown>, indent + 1)
|
|
334
|
-
} else if (Array.isArray(value)) {
|
|
335
|
-
result += `${spaces}${key}:\n`
|
|
336
|
-
for (const item of value) {
|
|
337
|
-
if (typeof item === 'object' && item !== null) {
|
|
338
|
-
result += `${spaces}- \n`
|
|
339
|
-
const itemYaml = jsonToYaml(item as Record<string, unknown>, indent + 2)
|
|
340
|
-
result += itemYaml
|
|
341
|
-
} else {
|
|
342
|
-
result += `${spaces}- ${formatValue(item)}\n`
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
} else {
|
|
346
|
-
result += `${spaces}${key}: ${formatValue(value)}\n`
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return result
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function formatValue(value: unknown): string {
|
|
354
|
-
if (typeof value === 'string') {
|
|
355
|
-
// Quote strings that contain special characters
|
|
356
|
-
if (value.includes(':') || value.includes('#') || value.includes('\n') || value.startsWith(' ') || value.endsWith(' ')) {
|
|
357
|
-
return `"${value.replace(/"/g, '\\"')}"`
|
|
358
|
-
}
|
|
359
|
-
return value
|
|
360
|
-
}
|
|
361
|
-
return String(value)
|
|
355
|
+
function jsonToYaml(obj: Record<string, unknown>): string {
|
|
356
|
+
if (!obj || Object.keys(obj).length === 0) return ''
|
|
357
|
+
return yaml.stringify(obj, { lineWidth: 0 })
|
|
362
358
|
}
|
|
@@ -35,3 +35,7 @@ export function getChartDisplay(chart: string, version: string): string {
|
|
|
35
35
|
|
|
36
36
|
// Re-export kindToPlural from centralized navigation utils
|
|
37
37
|
export { kindToPlural } from '../../utils/navigation'
|
|
38
|
+
|
|
39
|
+
// Re-export from k8s-ui where the predicate lives next to the
|
|
40
|
+
// Helm status color palette and is unit-tested.
|
|
41
|
+
export { isHelmReleaseActionable } from '../../utils/badge-colors'
|
|
@@ -82,6 +82,9 @@ export function ActivitySummary({ namespaces, topology, onNavigate }: ActivitySu
|
|
|
82
82
|
limit: 1000,
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
// Intentionally re-sample 'now' only when events refresh (not every render),
|
|
86
|
+
// so the timeline window stays stable between data updates.
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
88
|
const now = useMemo(() => Date.now(), [events])
|
|
86
89
|
const spanMs = SPAN_MINUTES * 60 * 1000
|
|
87
90
|
const startTime = now - spanMs
|
|
@@ -125,7 +128,7 @@ export function ActivitySummary({ namespaces, topology, onNavigate }: ActivitySu
|
|
|
125
128
|
<div className="flex-1 min-h-0 overflow-hidden px-4 py-1.5">
|
|
126
129
|
{isLoading ? (
|
|
127
130
|
<div className="flex items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
|
|
128
|
-
Loading
|
|
131
|
+
Loading…
|
|
129
132
|
</div>
|
|
130
133
|
) : error ? (
|
|
131
134
|
<div className="flex items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
|