@skyhook-io/radar-app 1.8.6 → 1.8.8

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.
Files changed (109) hide show
  1. package/README.md +7 -1
  2. package/package.json +12 -10
  3. package/src/App.tsx +278 -230
  4. package/src/RadarApp.tsx +91 -20
  5. package/src/api/client.argoResourceSync.test.ts +69 -0
  6. package/src/api/client.delta.test.ts +89 -0
  7. package/src/api/client.deltaSync.test.ts +216 -0
  8. package/src/api/client.rightsizing.test.ts +32 -0
  9. package/src/api/client.ts +1462 -246
  10. package/src/api/diagnose.ts +288 -0
  11. package/src/api/timelineSource.test.ts +217 -0
  12. package/src/api/timelineSource.ts +582 -0
  13. package/src/components/ConnectionErrorView.tsx +174 -70
  14. package/src/components/ContextSwitcher.tsx +13 -5
  15. package/src/components/applications/ApplicationsView.tsx +775 -80
  16. package/src/components/audit/AuditSettingsDialog.tsx +2 -2
  17. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  18. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  19. package/src/components/cost/CostTrendChart.tsx +103 -72
  20. package/src/components/cost/CostView.test.ts +12 -0
  21. package/src/components/cost/CostView.tsx +494 -229
  22. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  23. package/src/components/cost/CostViewTabs.tsx +40 -0
  24. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  25. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  26. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  27. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  28. package/src/components/cost/cloud-console.test.ts +39 -0
  29. package/src/components/cost/cloud-console.ts +81 -0
  30. package/src/components/cost/errors.ts +8 -0
  31. package/src/components/cost/format.test.ts +27 -0
  32. package/src/components/cost/format.ts +46 -0
  33. package/src/components/cost/kinds.ts +5 -0
  34. package/src/components/curl/ServiceCurlButton.tsx +2 -2
  35. package/src/components/diagnose/AISettings.tsx +116 -0
  36. package/src/components/diagnose/DiagnoseContext.tsx +491 -0
  37. package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
  38. package/src/components/diagnose/Home.tsx +163 -0
  39. package/src/components/diagnose/InvestigationView.tsx +604 -0
  40. package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
  41. package/src/components/diagnose/launch.ts +65 -0
  42. package/src/components/diagnose/parts.tsx +1689 -0
  43. package/src/components/dock/BottomDock.tsx +2 -3
  44. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  45. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  46. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  47. package/src/components/execution/batch-run-actions.test.ts +48 -0
  48. package/src/components/execution/batch-run-actions.ts +24 -0
  49. package/src/components/execution/batch-timeline.test.ts +57 -0
  50. package/src/components/execution/batch-timeline.ts +46 -0
  51. package/src/components/execution/execution-definition.test.ts +208 -0
  52. package/src/components/execution/execution-definition.ts +245 -0
  53. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  54. package/src/components/gitops/GitOpsView.tsx +81 -14
  55. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  56. package/src/components/helm/ChartBrowser.tsx +2 -3
  57. package/src/components/helm/HelmCompareRoute.tsx +1 -2
  58. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  59. package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
  60. package/src/components/helm/HelmView.tsx +2 -3
  61. package/src/components/helm/InstallWizard.tsx +3 -5
  62. package/src/components/helm/ManifestDiffViewer.tsx +1 -31
  63. package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
  64. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  65. package/src/components/home/CostCard.tsx +21 -36
  66. package/src/components/home/HomeView.tsx +17 -15
  67. package/src/components/home/MCPSetupDialog.tsx +1 -1
  68. package/src/components/home/mcpToolCatalog.ts +2 -2
  69. package/src/components/issues/IssuesPane.tsx +9 -1
  70. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  71. package/src/components/portforward/PortForwardButton.tsx +2 -2
  72. package/src/components/portforward/PortForwardManager.tsx +19 -14
  73. package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
  74. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  75. package/src/components/resource/RightsizingStrip.tsx +320 -127
  76. package/src/components/resources/ImageFilesystemModal.tsx +2 -2
  77. package/src/components/resources/PodFilesystemModal.tsx +2 -3
  78. package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
  79. package/src/components/resources/ResourcesView.tsx +13 -4
  80. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  81. package/src/components/resources/renderers/index.ts +1 -0
  82. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  83. package/src/components/rightsizing/copy.test.ts +56 -0
  84. package/src/components/rightsizing/model.test.ts +227 -0
  85. package/src/components/rightsizing/model.ts +158 -0
  86. package/src/components/rightsizing/presentation.test.ts +104 -0
  87. package/src/components/rightsizing/presentation.ts +94 -0
  88. package/src/components/settings/MyPermissionsDialog.tsx +66 -116
  89. package/src/components/settings/SettingsDialog.tsx +1290 -283
  90. package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
  91. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  92. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  93. package/src/components/timeline/TimelineList.tsx +81 -13
  94. package/src/components/timeline/TimelineView.tsx +842 -26
  95. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  96. package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
  97. package/src/components/traffic/TrafficView.tsx +8 -4
  98. package/src/components/ui/CommandPalette.tsx +2 -2
  99. package/src/components/workload/WorkloadView.tsx +912 -245
  100. package/src/context/ConnectionContext.tsx +109 -11
  101. package/src/context/DiagnoseCustomization.tsx +42 -0
  102. package/src/context/TimelineSource.tsx +50 -0
  103. package/src/hooks/useClusterLoadState.ts +73 -0
  104. package/src/hooks/useEventSource.ts +6 -0
  105. package/src/index.css +162 -1
  106. package/src/index.ts +12 -0
  107. package/src/main.tsx +1 -1
  108. package/src/types/clusterLoadState.ts +33 -0
  109. package/src/utils/navigation.ts +10 -0
@@ -19,6 +19,7 @@ import {
19
19
  formatGitOpsSourceUrl,
20
20
  getGitOpsResourceStatus,
21
21
  getGitOpsTool,
22
+ isArgoOperationInProgress,
22
23
  isArgoSuspendedByRadar,
23
24
  gitOpsInsightChangeKey,
24
25
  initNavigationMap,
@@ -46,8 +47,10 @@ import { useToast } from '../ui/Toast'
46
47
 
47
48
  import {
48
49
  fetchJSON,
50
+ buildArgoResourceSyncVars,
49
51
  useApplyResource,
50
52
  useArgoRefresh,
53
+ useArgoResourceValidation,
51
54
  useArgoResume,
52
55
  useArgoRollback,
53
56
  useArgoSuspend,
@@ -66,6 +69,8 @@ import { useConnection } from '../../context/ConnectionContext'
66
69
  import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
67
70
  import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
68
71
  import { CodeViewer } from '../ui/CodeViewer'
72
+ import { ArgoResourceDiffLoader } from './ArgoResourceDiffLoader'
73
+ import { RevisionMetaChip } from './RevisionMetaChip'
69
74
  import type { GitOpsHistoryItem } from '@skyhook-io/k8s-ui'
70
75
 
71
76
  const GITOPS_KINDS: APIResource[] = [
@@ -80,6 +85,10 @@ const GITOPS_KINDS: APIResource[] = [
80
85
  { name: 'alerts', kind: 'Alert', group: 'notification.toolkit.fluxcd.io', version: 'v1beta3', namespaced: true, verbs: ['list', 'get'], isCrd: true },
81
86
  ]
82
87
 
88
+ type ArgoSyncDialogTarget =
89
+ | { scope: 'application' }
90
+ | { scope: 'resource'; resource: GitOpsInsightRef }
91
+
83
92
  const KIND_BY_NAME = new Map(GITOPS_KINDS.map((k) => [k.name, k]))
84
93
 
85
94
  // Rows are the table's primary content; their poll cadence is what the toolbar
@@ -97,12 +106,15 @@ interface GitOpsViewProps {
97
106
  namespaces: string[]
98
107
  onOpenResource: (resource: SelectedResource) => void
99
108
  onClearNamespaces?: () => void
109
+ // Opens the global Settings dialog — backs the "Connect Argo CD" hint on the
110
+ // Changes tab of an Argo Application detail page.
111
+ onOpenSettings?: () => void
100
112
  }
101
113
 
102
- export function GitOpsView({ namespaces, onOpenResource, onClearNamespaces }: GitOpsViewProps) {
114
+ export function GitOpsView({ namespaces, onOpenResource, onClearNamespaces, onOpenSettings }: GitOpsViewProps) {
103
115
  const location = useLocation()
104
116
  if (location.pathname.startsWith('/gitops/detail/')) {
105
- return <GitOpsDetailView namespaces={namespaces} onOpenResource={onOpenResource} />
117
+ return <GitOpsDetailView namespaces={namespaces} onOpenResource={onOpenResource} onOpenSettings={onOpenSettings} />
106
118
  }
107
119
  return <GitOpsTableView namespaces={namespaces} onClearNamespaces={onClearNamespaces} />
108
120
  }
@@ -329,7 +341,7 @@ function GitOpsTableView({ namespaces, onClearNamespaces }: { namespaces: string
329
341
  )
330
342
  }
331
343
 
332
- function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
344
+ function GitOpsDetailView({ namespaces, onOpenResource, onOpenSettings }: GitOpsViewProps) {
333
345
  const location = useLocation()
334
346
  const navigate = useNavigate()
335
347
  const { showError, showSuccess } = useToast()
@@ -403,6 +415,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
403
415
  const [helmValuesOpen, setHelmValuesOpen] = useState(false)
404
416
 
405
417
  const argoSync = useArgoSync()
418
+ const argoResourceValidation = useArgoResourceValidation()
406
419
  const argoRefresh = useArgoRefresh()
407
420
  const argoTerminate = useArgoTerminate()
408
421
  const argoSuspend = useArgoSuspend()
@@ -414,13 +427,23 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
414
427
  const fluxSuspend = useFluxSuspend()
415
428
  const fluxResume = useFluxResume()
416
429
 
417
- const [syncDialogOpen, setSyncDialogOpen] = useState(false)
430
+ const [syncDialogTarget, setSyncDialogTarget] = useState<ArgoSyncDialogTarget | null>(null)
418
431
  // Doubles as the "open" flag (truthy = dialog open) and the data carrier
419
432
  // for which history entry to roll back to.
420
433
  const [rollbackTarget, setRollbackTarget] = useState<GitOpsHistoryItem | null>(null)
421
434
  // Disambiguates which refresh button is in flight (both share argoRefresh).
422
435
  const [refreshKind, setRefreshKind] = useState<'normal' | 'hard'>('normal')
423
436
 
437
+ function openArgoSyncDialog(target: ArgoSyncDialogTarget) {
438
+ argoResourceValidation.reset()
439
+ setSyncDialogTarget(target)
440
+ }
441
+
442
+ function closeArgoSyncDialog() {
443
+ argoResourceValidation.reset()
444
+ setSyncDialogTarget(null)
445
+ }
446
+
424
447
  const detailRow = resourceQ.data ? normalizeDetailResource(kind, group, resourceQ.data) : null
425
448
  const tree = treeQ.data ?? null
426
449
  const helmValues = useMemo(() => extractHelmValues(kind, resourceQ.data), [kind, resourceQ.data])
@@ -436,6 +459,13 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
436
459
  function openResourceFromTree(ref: GitOpsTreeRef | GitOpsInsightRef) {
437
460
  if (isGitOpsDetailRef(ref) && isValidKubernetesName(ref.name)) {
438
461
  const detailKind = kindToPlural(ref.kind)
462
+ // The tree's root node is this page's own subject — clicking it must not
463
+ // open a nested copy of the same detail page (which stacks an identical
464
+ // "GitOps / X / X" breadcrumb, and again, ad infinitum). A self-reference
465
+ // is a no-op; the header already represents this resource.
466
+ if (detailKind === kind && (ref.namespace || '') === (namespace || '') && ref.name === name) {
467
+ return
468
+ }
439
469
  const params = new URLSearchParams()
440
470
  if (ref.group) params.set('apiGroup', ref.group)
441
471
  // Lineage breadcrumb support: when the user opens a child GitOps CR
@@ -457,13 +487,14 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
457
487
  }
458
488
 
459
489
  const isRunning = resourceQ.data?.status?.operationState?.phase === 'Running'
490
+ const operationInProgress = isArgoOperationInProgress(resourceQ.data)
460
491
  const isFluxWorkload = kind === 'kustomizations' || kind === 'helmreleases'
461
492
  const isFlux = tool === 'flux'
462
493
  const isArgoApp = kind === 'applications'
463
494
 
464
495
  // Detail-page shortcuts. Skip when a modal is already open so a stray "s"
465
496
  // in an input field doesn't pop another sync dialog.
466
- const shortcutsEnabled = !syncDialogOpen && !rollbackTarget
497
+ const shortcutsEnabled = !syncDialogTarget && !rollbackTarget
467
498
  useRegisterShortcut({
468
499
  id: 'gitops-detail-sync',
469
500
  keys: 's',
@@ -471,11 +502,11 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
471
502
  category: 'GitOps',
472
503
  scope: 'gitops',
473
504
  handler: () => {
474
- if (effectiveSuspended || terminating) return
475
- if (isArgoApp) setSyncDialogOpen(true)
505
+ if (effectiveSuspended || terminating || operationInProgress) return
506
+ if (isArgoApp) openArgoSyncDialog({ scope: 'application' })
476
507
  else if (isFlux) fluxReconcile.mutate({ kind, namespace, name })
477
508
  },
478
- enabled: shortcutsEnabled && (isArgoApp || isFlux) && !effectiveSuspended && !terminating,
509
+ enabled: shortcutsEnabled && (isArgoApp || isFlux) && !effectiveSuspended && !terminating && !(isArgoApp && operationInProgress),
479
510
  })
480
511
  useRegisterShortcut({
481
512
  id: 'gitops-detail-refresh',
@@ -529,7 +560,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
529
560
  }
530
561
 
531
562
  const argoHandlers: ArgoActionHandlers | undefined = isArgoApp ? {
532
- onSyncRequested: () => setSyncDialogOpen(true),
563
+ onSyncRequested: () => openArgoSyncDialog({ scope: 'application' }),
533
564
  onRefresh: (refreshType) => {
534
565
  setRefreshKind(refreshType)
535
566
  argoRefresh.mutate({ namespace, name, hard: refreshType === 'hard' })
@@ -545,6 +576,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
545
576
  resuming: argoResume.isPending,
546
577
  autoSyncEnabled: argoAutoSyncEnabled,
547
578
  isRunning,
579
+ operationInProgress,
548
580
  } : undefined
549
581
 
550
582
  const fluxHandlers: FluxActionHandlers | undefined = isFlux ? {
@@ -576,6 +608,13 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
576
608
  detail={detail}
577
609
  insight={insightsQ.data ?? null}
578
610
  insightLoading={insightsQ.isLoading}
611
+ renderRevisionMeta={
612
+ isArgoApp && insightsQ.data?.capabilities?.revisionMetadataAvailable
613
+ ? (revision) => (
614
+ <RevisionMetaChip appNamespace={namespace} appName={name} revision={revision} />
615
+ )
616
+ : undefined
617
+ }
579
618
  onSelectIssue={(issue) => {
580
619
  const ref = issue.refs?.[0]
581
620
  if (!ref) return
@@ -679,7 +718,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
679
718
  <GitOpsActivityInsightView
680
719
  insight={insightsQ.data}
681
720
  error={insightsQ.error as Error | null}
682
- onRollback={isArgoApp ? (item) => {
721
+ onRollback={isArgoApp && !operationInProgress ? (item) => {
683
722
  if (parseArgoRollbackID(item.id) == null) return
684
723
  setRollbackTarget(item)
685
724
  } : undefined}
@@ -692,8 +731,22 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
692
731
  insight={insightsQ.data}
693
732
  error={insightsQ.error as Error | null}
694
733
  onOpenResource={openResourceFromTree}
734
+ onSyncResource={isArgoApp ? (resource) => openArgoSyncDialog({ scope: 'resource', resource }) : undefined}
735
+ syncResourceDisabledReason={isArgoApp ? (
736
+ terminating
737
+ ? terminatingActionTooltip
738
+ : effectiveSuspended
739
+ ? 'Resume the Application before syncing a resource.'
740
+ : operationInProgress || argoSync.isPending
741
+ ? 'Wait for the current sync operation to finish.'
742
+ : undefined
743
+ ) : undefined}
695
744
  focusKey={changesFocusKey}
696
745
  tree={tree}
746
+ renderResourceDiff={isArgoApp ? (ref) => (
747
+ <ArgoResourceDiffLoader appNamespace={namespace} appName={name} resourceRef={ref} />
748
+ ) : undefined}
749
+ onOpenSettings={onOpenSettings}
697
750
  />
698
751
  )
699
752
  }
@@ -739,13 +792,27 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
739
792
  {isArgoApp && (
740
793
  <>
741
794
  <SyncOptionsDialog
742
- open={syncDialogOpen}
795
+ open={!!syncDialogTarget}
743
796
  appLabel={`${namespace}/${name}`}
797
+ resource={syncDialogTarget?.scope === 'resource' ? syncDialogTarget.resource : undefined}
744
798
  pending={argoSync.isPending}
745
- onCancel={() => setSyncDialogOpen(false)}
799
+ autoSyncEnabled={argoAutoSyncEnabled}
800
+ validationPending={argoResourceValidation.isPending}
801
+ operationInProgress={operationInProgress}
802
+ validationResult={argoResourceValidation.data}
803
+ validationError={argoResourceValidation.error?.message}
804
+ onCancel={closeArgoSyncDialog}
805
+ onValidationReset={() => argoResourceValidation.reset()}
806
+ onValidate={syncDialogTarget?.scope === 'resource' ? (opts) => {
807
+ argoResourceValidation.mutate(buildArgoResourceSyncVars(namespace, name, syncDialogTarget.resource, opts))
808
+ } : undefined}
746
809
  onConfirm={(opts) => {
747
- argoSync.mutate({ namespace, name, ...opts }, {
748
- onSettled: () => setSyncDialogOpen(false),
810
+ if (!syncDialogTarget) return
811
+ const variables = syncDialogTarget.scope === 'resource'
812
+ ? buildArgoResourceSyncVars(namespace, name, syncDialogTarget.resource, opts)
813
+ : { namespace, name, ...opts }
814
+ argoSync.mutate(variables, {
815
+ onSettled: closeArgoSyncDialog,
749
816
  })
750
817
  }}
751
818
  />
@@ -0,0 +1,63 @@
1
+ import { ShieldCheck, ShieldAlert } from 'lucide-react'
2
+ import type { ReactNode } from 'react'
3
+ import { useArgoRevisionMetadata } from '../../api/client'
4
+ import { Tooltip } from '../ui/Tooltip'
5
+
6
+ // Argo returns the author as "Name <email>"; show just the name.
7
+ function authorName(author?: string): string {
8
+ if (!author) return ''
9
+ const lt = author.indexOf('<')
10
+ return (lt >= 0 ? author.slice(0, lt) : author).trim()
11
+ }
12
+
13
+ // signatureInfo is a raw GPG verification line. "Good signature" → verified;
14
+ // any other non-empty value → a check ran but didn't verify; empty → unsigned.
15
+ function signatureState(sig?: string): 'good' | 'bad' | null {
16
+ if (!sig) return null
17
+ return /good signature/i.test(sig) ? 'good' : 'bad'
18
+ }
19
+
20
+ // RevisionMetaChip hydrates the Argo status-strip revision with Git commit
21
+ // detail (author, subject, signature) fetched from the Argo CD API. Renders
22
+ // nothing until data arrives, so the bare SHA is never blocked; the whole thing
23
+ // is gated by the host on capabilities.revisionMetadataAvailable.
24
+ export function RevisionMetaChip({
25
+ appNamespace,
26
+ appName,
27
+ revision,
28
+ }: {
29
+ appNamespace: string
30
+ appName: string
31
+ revision: string
32
+ }): ReactNode {
33
+ // The host only renders this chip when revision metadata is available, so no
34
+ // extra enabled gate is needed — the hook's own appName/revision guard suffices.
35
+ const { data } = useArgoRevisionMetadata(appNamespace, appName, revision)
36
+ if (!data) return null
37
+
38
+ const author = authorName(data.author)
39
+ const sig = signatureState(data.signatureInfo)
40
+ const subject = data.message?.split('\n')[0]?.trim()
41
+ if (!author && !sig && !subject) return null
42
+
43
+ return (
44
+ <>
45
+ {author && <span className="shrink-0 text-theme-text-secondary">· {author}</span>}
46
+ {subject && (
47
+ <Tooltip content={data.message ?? subject} delay={300} wrapperClassName="min-w-0">
48
+ <span className="max-w-[32ch] truncate text-theme-text-tertiary">“{subject}”</span>
49
+ </Tooltip>
50
+ )}
51
+ {sig === 'good' && (
52
+ <Tooltip content={data.signatureInfo ?? 'Signed commit'} delay={300} wrapperClassName="inline-flex shrink-0">
53
+ <ShieldCheck className="h-3 w-3 text-green-600 dark:text-green-400/80" />
54
+ </Tooltip>
55
+ )}
56
+ {sig === 'bad' && (
57
+ <Tooltip content={data.signatureInfo ?? 'Signature not verified'} delay={300} wrapperClassName="inline-flex shrink-0">
58
+ <ShieldAlert className="h-3 w-3 text-amber-600 dark:text-amber-400/80" />
59
+ </Tooltip>
60
+ )}
61
+ </>
62
+ )
63
+ }
@@ -1,6 +1,6 @@
1
1
  import { useState, useMemo } from 'react'
2
2
  import { Search, RefreshCw, Package, Database, AlertCircle, ExternalLink, ChevronDown, Star, Shield, BadgeCheck, Building2, Globe, ArrowUpDown, FileJson, PenTool } from 'lucide-react'
3
- import { PaneLoader } from '@skyhook-io/k8s-ui'
3
+ import { PaneLoader, Input } from '@skyhook-io/k8s-ui'
4
4
  import { clsx } from 'clsx'
5
5
  import { useHelmRepositories, useSearchCharts, useUpdateRepository, useUpdateRepositorySilent, useArtifactHubSearch, type ArtifactHubSortOption } from '../../api/client'
6
6
  import { useCanHelmWrite } from '../../contexts/CapabilitiesContext'
@@ -254,8 +254,7 @@ export function ChartBrowser({ onChartSelect }: ChartBrowserProps) {
254
254
  {/* Search */}
255
255
  <div className="flex-1 relative">
256
256
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
257
- <input
258
- type="text"
257
+ <Input
259
258
  placeholder={chartSource === 'local' ? "Search charts..." : "Search ArtifactHub..."}
260
259
  value={searchTerm}
261
260
  onChange={(e) => setSearchTerm(e.target.value)}
@@ -17,7 +17,7 @@ import {
17
17
  Package,
18
18
  Settings,
19
19
  } from 'lucide-react'
20
- import { PaneLoader } from '@skyhook-io/k8s-ui'
20
+ import { PaneLoader, DiffLine, hasDiffBodyChange } from '@skyhook-io/k8s-ui'
21
21
  import {
22
22
  useCloudRole,
23
23
  useHelmHooksDiff,
@@ -30,7 +30,6 @@ import {
30
30
  import type { HelmHook, HelmRevision, HooksDiff, ResourceDiff } from '../../types'
31
31
  import { getHelmStatusColor, getKindBadgeColor, SEVERITY_BADGE } from '../../utils/badge-colors'
32
32
  import { formatDate } from './helm-utils'
33
- import { DiffLine, hasDiffBodyChange } from './ManifestDiffViewer'
34
33
  import { RoleGatedPanel } from './RoleGatedPanel'
35
34
  import { Tooltip } from '../ui/Tooltip'
36
35
 
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isUpgradeSourceIssueActionable } from './HelmReleaseDrawer'
3
+
4
+ describe('isUpgradeSourceIssueActionable', () => {
5
+ it('keeps classic repository ambiguity informational', () => {
6
+ expect(isUpgradeSourceIssueActionable('ambiguous_repository')).toBe(false)
7
+ })
8
+
9
+ it('lets OCI registration help untracked and repo-index states', () => {
10
+ expect(isUpgradeSourceIssueActionable('untracked')).toBe(true)
11
+ expect(isUpgradeSourceIssueActionable('repo_index_error')).toBe(true)
12
+ })
13
+
14
+ it('does not treat a missing reason code as actionable', () => {
15
+ expect(isUpgradeSourceIssueActionable(undefined)).toBe(false)
16
+ })
17
+ })