@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.
Files changed (177) hide show
  1. package/README.md +7 -1
  2. package/package.json +33 -25
  3. package/src/App.tsx +1449 -382
  4. package/src/RadarApp.tsx +132 -19
  5. package/src/api/apiResources.ts +1 -1
  6. package/src/api/client.argoResourceSync.test.ts +69 -0
  7. package/src/api/client.delta.test.ts +89 -0
  8. package/src/api/client.deltaSync.test.ts +216 -0
  9. package/src/api/client.metrics.test.ts +106 -0
  10. package/src/api/client.rightsizing.test.ts +32 -0
  11. package/src/api/client.ts +2730 -271
  12. package/src/api/client.yaml.test.ts +45 -0
  13. package/src/api/diagnose.ts +289 -0
  14. package/src/api/quotas.ts +16 -0
  15. package/src/api/rbac.ts +57 -0
  16. package/src/api/timelineSource.test.ts +217 -0
  17. package/src/api/timelineSource.ts +582 -0
  18. package/src/components/ConnectionErrorView.tsx +186 -70
  19. package/src/components/ContextSwitcher.tsx +63 -18
  20. package/src/components/DebugOverlay.tsx +5 -3
  21. package/src/components/NamespaceSwitcher.tsx +41 -0
  22. package/src/components/UserMenu.tsx +69 -21
  23. package/src/components/applications/ApplicationsView.tsx +936 -0
  24. package/src/components/audit/AuditSettingsDialog.tsx +79 -17
  25. package/src/components/audit/AuditView.tsx +65 -62
  26. package/src/components/compare/CompareViewRoute.tsx +124 -0
  27. package/src/components/compare/useCompareCandidates.ts +27 -0
  28. package/src/components/compare/useCompareLauncher.tsx +79 -0
  29. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  30. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  31. package/src/components/cost/CostTrendChart.tsx +106 -75
  32. package/src/components/cost/CostView.test.ts +12 -0
  33. package/src/components/cost/CostView.tsx +507 -223
  34. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  35. package/src/components/cost/CostViewTabs.tsx +40 -0
  36. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  37. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  38. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  39. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  40. package/src/components/cost/cloud-console.test.ts +39 -0
  41. package/src/components/cost/cloud-console.ts +81 -0
  42. package/src/components/cost/errors.ts +8 -0
  43. package/src/components/cost/format.test.ts +27 -0
  44. package/src/components/cost/format.ts +46 -0
  45. package/src/components/cost/kinds.ts +5 -0
  46. package/src/components/curl/ServiceCurlButton.tsx +445 -0
  47. package/src/components/diagnose/AISettings.tsx +147 -0
  48. package/src/components/diagnose/DiagnoseContext.tsx +495 -0
  49. package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
  50. package/src/components/diagnose/Home.tsx +163 -0
  51. package/src/components/diagnose/InvestigationView.tsx +622 -0
  52. package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
  53. package/src/components/diagnose/launch.ts +65 -0
  54. package/src/components/diagnose/parts.tsx +1756 -0
  55. package/src/components/dock/BottomDock.tsx +2 -3
  56. package/src/components/dock/DockContext.tsx +1 -0
  57. package/src/components/dock/TerminalTab.tsx +1 -1
  58. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  59. package/src/components/dock/index.ts +1 -1
  60. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  61. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  62. package/src/components/execution/batch-run-actions.test.ts +48 -0
  63. package/src/components/execution/batch-run-actions.ts +24 -0
  64. package/src/components/execution/batch-timeline.test.ts +57 -0
  65. package/src/components/execution/batch-timeline.ts +46 -0
  66. package/src/components/execution/execution-definition.test.ts +208 -0
  67. package/src/components/execution/execution-definition.ts +245 -0
  68. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  69. package/src/components/gitops/GitOpsView.tsx +1042 -0
  70. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  71. package/src/components/helm/ChartBrowser.tsx +87 -31
  72. package/src/components/helm/HelmCompareRoute.tsx +1341 -0
  73. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  74. package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
  75. package/src/components/helm/HelmView.tsx +237 -96
  76. package/src/components/helm/InstallWizard.tsx +94 -38
  77. package/src/components/helm/ManifestDiffViewer.tsx +8 -27
  78. package/src/components/helm/OwnedResources.tsx +34 -59
  79. package/src/components/helm/RevisionHistory.tsx +52 -3
  80. package/src/components/helm/RoleGatedPanel.tsx +3 -3
  81. package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
  82. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  83. package/src/components/helm/ValuesViewer.tsx +49 -53
  84. package/src/components/helm/helm-utils.ts +4 -0
  85. package/src/components/home/ActivitySummary.tsx +4 -1
  86. package/src/components/home/ClusterHealthCard.tsx +56 -42
  87. package/src/components/home/CostCard.tsx +21 -36
  88. package/src/components/home/GitOpsControllersCard.tsx +110 -0
  89. package/src/components/home/HelmSummary.tsx +3 -1
  90. package/src/components/home/HomeView.tsx +339 -105
  91. package/src/components/home/MCPSetupDialog.tsx +29 -87
  92. package/src/components/home/TrafficSummary.tsx +2 -2
  93. package/src/components/home/mcpToolCatalog.ts +333 -0
  94. package/src/components/issues/IssuesPane.tsx +151 -0
  95. package/src/components/logs/LogsViewer.tsx +4 -1
  96. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  97. package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
  98. package/src/components/nav/PrimaryNavRail.tsx +285 -0
  99. package/src/components/portforward/PortForwardButton.tsx +118 -47
  100. package/src/components/portforward/PortForwardManager.tsx +253 -131
  101. package/src/components/resource/HPACharts.tsx +237 -0
  102. package/src/components/resource/PVCUsageBar.tsx +59 -0
  103. package/src/components/resource/PrometheusCharts.tsx +160 -584
  104. package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
  105. package/src/components/resource/RestartChart.tsx +133 -0
  106. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  107. package/src/components/resource/RightsizingStrip.tsx +363 -0
  108. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  109. package/src/components/resources/CompositeRenderer.tsx +101 -0
  110. package/src/components/resources/ImageFilesystemModal.tsx +19 -12
  111. package/src/components/resources/PodFilesystemModal.tsx +6 -5
  112. package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
  113. package/src/components/resources/ResourcesView.tsx +194 -17
  114. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/HPARenderer.tsx +20 -1
  116. package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
  117. package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
  118. package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
  119. package/src/components/resources/renderers/PodRenderer.tsx +30 -6
  120. package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
  121. package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
  122. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
  123. package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
  124. package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
  125. package/src/components/resources/renderers/index.ts +2 -0
  126. package/src/components/resources/resource-utils.ts +2 -1
  127. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  128. package/src/components/rightsizing/copy.test.ts +56 -0
  129. package/src/components/rightsizing/model.test.ts +227 -0
  130. package/src/components/rightsizing/model.ts +158 -0
  131. package/src/components/rightsizing/presentation.test.ts +104 -0
  132. package/src/components/rightsizing/presentation.ts +94 -0
  133. package/src/components/settings/MyPermissionsDialog.tsx +241 -0
  134. package/src/components/settings/SettingsDialog.tsx +1505 -165
  135. package/src/components/shared/CreateResourceDialog.tsx +9 -2
  136. package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
  137. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  138. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  139. package/src/components/timeline/TimelineList.tsx +86 -13
  140. package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
  141. package/src/components/timeline/TimelineView.tsx +873 -24
  142. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  143. package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
  144. package/src/components/traffic/TrafficFlowList.tsx +29 -15
  145. package/src/components/traffic/TrafficGraph.tsx +42 -24
  146. package/src/components/traffic/TrafficView.tsx +32 -19
  147. package/src/components/ui/CommandPalette.tsx +8 -215
  148. package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
  149. package/src/components/ui/Markdown.tsx +3 -3
  150. package/src/components/ui/Omnibar.tsx +602 -0
  151. package/src/components/ui/RadarOmnibar.tsx +52 -0
  152. package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
  153. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
  154. package/src/components/ui/UpdateNotification.tsx +48 -36
  155. package/src/components/ui/command-items.ts +178 -0
  156. package/src/components/workload/WorkloadView.tsx +1342 -158
  157. package/src/context/ConnectionContext.tsx +146 -21
  158. package/src/context/DiagnoseCustomization.tsx +93 -0
  159. package/src/context/NavCustomization.tsx +75 -0
  160. package/src/context/TimelineSource.tsx +50 -0
  161. package/src/contexts/CapabilitiesContext.tsx +32 -8
  162. package/src/filter/FilterLocationBridge.tsx +30 -0
  163. package/src/hooks/useClusterLoadState.ts +73 -0
  164. package/src/hooks/useDocumentTitle.ts +25 -0
  165. package/src/hooks/useEventSource.ts +6 -0
  166. package/src/hooks/useKeyboardShortcuts.tsx +1 -0
  167. package/src/hooks/useMediaQuery.ts +21 -0
  168. package/src/hooks/useNavRailPinned.ts +46 -0
  169. package/src/hooks/useRecentResources.ts +49 -0
  170. package/src/index.css +162 -1
  171. package/src/index.ts +73 -1
  172. package/src/main.tsx +7 -5
  173. package/src/types/clusterLoadState.ts +33 -0
  174. package/src/types.ts +2 -0
  175. package/src/utils/auditBadges.ts +53 -0
  176. package/src/utils/navigation.ts +64 -1
  177. package/src/components/ui/NamespaceSelector.tsx +0 -436
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
2
2
  import { X, Package, ChevronRight, ChevronLeft, Play, Loader2, AlertTriangle, CheckCircle, User, BookOpen, Link as LinkIcon, Star, BadgeCheck, Shield, Globe, Building2, Plus, Minus, Terminal } 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 yaml from 'yaml'
6
6
  import { createPatch } from 'diff'
@@ -12,6 +12,7 @@ import { YamlEditor } from '../ui/YamlEditor'
12
12
  import { Tooltip } from '../ui/Tooltip'
13
13
  import { Markdown } from '../ui/Markdown'
14
14
  import { SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
15
+ import { validateHelmReleaseName, validateRFC1123Label } from '@skyhook-io/k8s-ui/utils/validators'
15
16
 
16
17
  // Deep merge two objects — values from `overrides` take priority
17
18
  function deepMerge(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {
@@ -27,7 +28,6 @@ function deepMerge(base: Record<string, unknown>, overrides: Record<string, unkn
27
28
  }
28
29
  return result
29
30
  }
30
-
31
31
  interface InstallWizardProps {
32
32
  repo: string
33
33
  chartName: string
@@ -105,11 +105,11 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
105
105
  }
106
106
  }
107
107
  if (baseValues && defaultValues) {
108
- setValuesYaml(yaml.stringify(deepMerge(baseValues, defaultValues)))
108
+ setValuesYaml(yaml.stringify(deepMerge(baseValues, defaultValues), { lineWidth: 0 }))
109
109
  } else if (defaultValues && !baseValues) {
110
- setValuesYaml(yaml.stringify(defaultValues))
110
+ setValuesYaml(yaml.stringify(defaultValues, { lineWidth: 0 }))
111
111
  } else if (baseValues) {
112
- setValuesYaml(yaml.stringify(baseValues))
112
+ setValuesYaml(yaml.stringify(baseValues, { lineWidth: 0 }))
113
113
  }
114
114
  }, [localChartDetail?.values, artifactHubDetail?.values, isLocal, defaultValues])
115
115
 
@@ -134,11 +134,20 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
134
134
  setInstallError(null)
135
135
  setProgressLogs([])
136
136
 
137
+ // Send the same trimmed values the validators ran on, not the
138
+ // raw input. Without this, a release name with trailing
139
+ // whitespace passes the client-side validator (which calls
140
+ // `.trim()`) but the server receives the untrimmed string and
141
+ // rejects it — exactly the surprise this validation layer
142
+ // exists to prevent.
143
+ const trimmedReleaseName = releaseName.trim()
144
+ const trimmedNamespace = namespace.trim()
145
+
137
146
  try {
138
147
  const release = await installChartWithProgress(
139
148
  {
140
- releaseName,
141
- namespace,
149
+ releaseName: trimmedReleaseName,
150
+ namespace: trimmedNamespace,
142
151
  chartName,
143
152
  version,
144
153
  repository,
@@ -169,7 +178,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
169
178
 
170
179
  // Wait a moment to show success, then close
171
180
  setTimeout(() => {
172
- onSuccess(namespace, releaseName)
181
+ onSuccess(trimmedNamespace, trimmedReleaseName)
173
182
  }, 1500)
174
183
  } catch (err) {
175
184
  setInstallError(err instanceof Error ? err.message : 'Install failed')
@@ -181,9 +190,25 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
181
190
  } finally {
182
191
  setIsInstalling(false)
183
192
  }
184
- }, [releaseName, namespace, chartName, version, repo, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
185
-
186
- const canProceedFromInfo = releaseName.trim() !== '' && namespace.trim() !== ''
193
+ }, [releaseName, namespace, chartName, version, repo, repoUrl, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
194
+
195
+ // Validate release name + namespace before letting the user
196
+ // advance. Without this, a name like "Invalid Name With Spaces!"
197
+ // was accepted through to step 2 and only failed server-side at
198
+ // install time — the server returned a 422 the user couldn't
199
+ // connect back to anything they typed. K8s / Helm rules are
200
+ // pinned in packages/k8s-ui/src/utils/validators.ts.
201
+ const releaseNameValidation = useMemo(
202
+ () => validateHelmReleaseName(releaseName.trim()),
203
+ [releaseName],
204
+ )
205
+ const namespaceValidation = useMemo(
206
+ () => validateRFC1123Label(namespace.trim()),
207
+ [namespace],
208
+ )
209
+ const releaseNameError = releaseNameValidation.valid ? null : releaseNameValidation.error
210
+ const namespaceError = namespaceValidation.valid ? null : namespaceValidation.error
211
+ const canProceedFromInfo = releaseNameValidation.valid && namespaceValidation.valid
187
212
  const canInstall = canProceedFromInfo && !yamlError
188
213
 
189
214
  const steps: { id: WizardStep; label: string }[] = [
@@ -206,7 +231,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
206
231
  <img
207
232
  src={isLocal ? (chartDetail as ChartDetail)?.icon : (chartDetail as ArtifactHubChartDetail)?.logoUrl}
208
233
  alt=""
209
- className="w-8 h-8 rounded object-contain bg-white/10 p-1"
234
+ className="w-8 h-8 rounded object-contain bg-theme-elevated p-1"
210
235
  />
211
236
  ) : (
212
237
  <Package className="w-8 h-8 text-purple-400" />
@@ -216,7 +241,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
216
241
  <h2 className="text-lg font-semibold text-theme-text-primary">Install {chartName}</h2>
217
242
  {!isLocal && (
218
243
  <Tooltip content="From ArtifactHub">
219
- <Globe className="w-4 h-4 text-blue-400" />
244
+ <Globe className="w-4 h-4 text-accent" />
220
245
  </Tooltip>
221
246
  )}
222
247
  </div>
@@ -224,7 +249,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
224
249
  <span>{repo} / {version}</span>
225
250
  {!isLocal && (chartDetail as ArtifactHubChartDetail)?.repository?.official && (
226
251
  <Tooltip content="Official">
227
- <BadgeCheck className="w-3.5 h-3.5 text-blue-400" />
252
+ <BadgeCheck className="w-3.5 h-3.5 text-accent" />
228
253
  </Tooltip>
229
254
  )}
230
255
  {!isLocal && (chartDetail as ArtifactHubChartDetail)?.repository?.verifiedPublisher && (
@@ -264,7 +289,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
264
289
  >
265
290
  <span className={clsx(
266
291
  'w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium',
267
- step === s.id ? 'bg-blue-500 text-white' : 'bg-theme-elevated text-theme-text-secondary'
292
+ step === s.id ? 'bg-accent text-white' : 'bg-theme-elevated text-theme-text-secondary'
268
293
  )}>
269
294
  {i + 1}
270
295
  </span>
@@ -287,8 +312,10 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
287
312
  source={source}
288
313
  releaseName={releaseName}
289
314
  setReleaseName={setReleaseName}
315
+ releaseNameError={releaseNameError}
290
316
  namespace={namespace}
291
317
  setNamespace={setNamespace}
318
+ namespaceError={namespaceError}
292
319
  namespaces={namespaces || []}
293
320
  createNamespace={createNamespace}
294
321
  setCreateNamespace={setCreateNamespace}
@@ -321,7 +348,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
321
348
  valuesYaml={valuesYaml}
322
349
  defaultValuesYaml={
323
350
  isLocal
324
- ? (localChartDetail?.values ? yaml.stringify(localChartDetail.values) : '')
351
+ ? (localChartDetail?.values ? yaml.stringify(localChartDetail.values, { lineWidth: 0 }) : '')
325
352
  : (artifactHubDetail?.values || '')
326
353
  }
327
354
  />
@@ -383,11 +410,11 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
383
410
  Cancel
384
411
  </button>
385
412
  {step === 'review' ? (
413
+ <Tooltip content={!canHelmWrite ? helmActReason : ''}>
386
414
  <button
387
415
  onClick={handleInstall}
388
416
  disabled={!canInstall || isInstalling || !canHelmWrite}
389
- className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed"
390
- title={!canHelmWrite ? helmActReason : undefined}
417
+ className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed disabled:pointer-events-none"
391
418
  >
392
419
  {isInstalling ? (
393
420
  <Loader2 className="w-4 h-4 animate-spin" />
@@ -396,6 +423,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
396
423
  )}
397
424
  Install
398
425
  </button>
426
+ </Tooltip>
399
427
  ) : (
400
428
  <button
401
429
  onClick={() => setStep(step === 'info' ? 'values' : 'review')}
@@ -420,8 +448,10 @@ interface InfoStepProps {
420
448
  source: ChartSource
421
449
  releaseName: string
422
450
  setReleaseName: (name: string) => void
451
+ releaseNameError: string | null
423
452
  namespace: string
424
453
  setNamespace: (ns: string) => void
454
+ namespaceError: string | null
425
455
  namespaces: { name: string }[]
426
456
  createNamespace: boolean
427
457
  setCreateNamespace: (create: boolean) => void
@@ -434,8 +464,10 @@ function InfoStep({
434
464
  source,
435
465
  releaseName,
436
466
  setReleaseName,
467
+ releaseNameError,
437
468
  namespace,
438
469
  setNamespace,
470
+ namespaceError,
439
471
  namespaces,
440
472
  createNamespace,
441
473
  setCreateNamespace,
@@ -506,7 +538,7 @@ function InfoStep({
506
538
  href={home}
507
539
  target="_blank"
508
540
  rel="noopener noreferrer"
509
- className="flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300"
541
+ className="flex items-center gap-1 text-xs text-accent-text hover:underline"
510
542
  >
511
543
  <LinkIcon className="w-3.5 h-3.5" />
512
544
  Homepage
@@ -528,16 +560,28 @@ function InfoStep({
528
560
  <label className="block text-sm font-medium text-theme-text-secondary mb-2">
529
561
  Release Name
530
562
  </label>
531
- <input
532
- type="text"
563
+ <Input
533
564
  value={releaseName}
534
565
  onChange={(e) => setReleaseName(e.target.value)}
535
566
  placeholder="my-release"
536
- className="w-full px-3 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-blue-500"
567
+ aria-invalid={releaseNameError ? true : undefined}
568
+ aria-describedby="release-name-help"
569
+ className={clsx(
570
+ 'w-full 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',
571
+ releaseNameError
572
+ ? 'border-red-500/60 focus:ring-red-500'
573
+ : 'border-theme-border-light focus:ring-accent',
574
+ )}
537
575
  />
538
- <p className="mt-1 text-xs text-theme-text-tertiary">
539
- A unique name for this release in the namespace
540
- </p>
576
+ {releaseNameError ? (
577
+ <p id="release-name-help" className="mt-1 text-xs text-red-400">
578
+ Release name {releaseNameError}.
579
+ </p>
580
+ ) : (
581
+ <p id="release-name-help" className="mt-1 text-xs text-theme-text-tertiary">
582
+ A unique name for this release in the namespace
583
+ </p>
584
+ )}
541
585
  </div>
542
586
 
543
587
  {/* Namespace selection */}
@@ -545,14 +589,25 @@ function InfoStep({
545
589
  <label className="block text-sm font-medium text-theme-text-secondary mb-2">
546
590
  Namespace
547
591
  </label>
548
- <input
549
- type="text"
592
+ <Input
550
593
  list="namespace-suggestions"
551
594
  value={namespace}
552
595
  onChange={(e) => setNamespace(e.target.value)}
553
596
  placeholder="Enter namespace name"
554
- className="w-full px-3 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-blue-500"
597
+ aria-invalid={namespaceError ? true : undefined}
598
+ aria-describedby="namespace-help"
599
+ className={clsx(
600
+ 'w-full 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',
601
+ namespaceError
602
+ ? 'border-red-500/60 focus:ring-red-500'
603
+ : 'border-theme-border-light focus:ring-accent',
604
+ )}
555
605
  />
606
+ {namespaceError && (
607
+ <p id="namespace-help" className="mt-1 text-xs text-red-400">
608
+ Namespace {namespaceError}.
609
+ </p>
610
+ )}
556
611
  <datalist id="namespace-suggestions">
557
612
  {namespaces.map(ns => (
558
613
  <option key={ns.name} value={ns.name} />
@@ -563,7 +618,7 @@ function InfoStep({
563
618
  type="checkbox"
564
619
  checked={createNamespace}
565
620
  onChange={(e) => setCreateNamespace(e.target.checked)}
566
- className="rounded border-theme-border text-blue-500 focus:ring-blue-500"
621
+ className="rounded border-theme-border text-accent focus:ring-accent"
567
622
  />
568
623
  Create namespace if it doesn't exist
569
624
  </label>
@@ -574,7 +629,7 @@ function InfoStep({
574
629
  <div>
575
630
  <button
576
631
  onClick={() => setShowReadme(!showReadme)}
577
- className="flex items-center gap-2 text-sm text-blue-400 hover:text-blue-300"
632
+ className="flex items-center gap-2 text-sm text-accent-text hover:underline"
578
633
  >
579
634
  <BookOpen className="w-4 h-4" />
580
635
  {showReadme ? 'Hide' : 'Show'} Chart README
@@ -607,7 +662,7 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
607
662
  const ahDetail = chartDetail as ArtifactHubChartDetail | undefined
608
663
 
609
664
  const defaultValues = isLocal
610
- ? (localDetail?.values ? yaml.stringify(localDetail.values) : '')
665
+ ? (localDetail?.values ? yaml.stringify(localDetail.values, { lineWidth: 0 }) : '')
611
666
  : (ahDetail?.values || '')
612
667
 
613
668
  const hasDefaults = Boolean(defaultValues)
@@ -619,8 +674,8 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
619
674
  return (
620
675
  <div className="space-y-4">
621
676
  {/* Info banner */}
622
- <div className="flex items-start gap-3 p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
623
- <CheckCircle className="w-5 h-5 text-blue-400 shrink-0 mt-0.5" />
677
+ <div className="flex items-start gap-3 p-4 bg-accent-muted border border-accent/30 rounded-lg">
678
+ <CheckCircle className="w-5 h-5 text-accent shrink-0 mt-0.5" />
624
679
  <div>
625
680
  <p className="text-sm font-medium text-theme-text-primary">Ready to install with defaults</p>
626
681
  <p className="text-xs text-theme-text-secondary mt-1">
@@ -633,7 +688,7 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
633
688
  href={homeUrl}
634
689
  target="_blank"
635
690
  rel="noopener noreferrer"
636
- className="inline-flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300 mt-2"
691
+ className="inline-flex items-center gap-1 text-xs text-accent-text hover:underline mt-2"
637
692
  >
638
693
  <LinkIcon className="w-3 h-3" />
639
694
  View chart documentation
@@ -701,6 +756,7 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
701
756
  <YamlEditor
702
757
  value={valuesYaml}
703
758
  onChange={setValuesYaml}
759
+ showProblems={false}
704
760
  height="300px"
705
761
  onValidate={(isValid, errors) => {
706
762
  setYamlError(isValid ? null : errors[0] || 'Invalid YAML')
@@ -819,8 +875,8 @@ function ReviewStep({
819
875
 
820
876
  return (
821
877
  <div className="space-y-6">
822
- <div className="flex items-center gap-3 p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
823
- <CheckCircle className="w-6 h-6 text-blue-400 shrink-0" />
878
+ <div className="flex items-center gap-3 p-4 bg-accent-muted border border-accent/30 rounded-lg">
879
+ <CheckCircle className="w-6 h-6 text-accent shrink-0" />
824
880
  <div>
825
881
  <p className="text-sm font-medium text-theme-text-primary">Ready to install</p>
826
882
  <p className="text-xs text-theme-text-secondary mt-0.5">
@@ -836,7 +892,7 @@ function ReviewStep({
836
892
  <div>
837
893
  <p className="text-sm font-medium text-amber-400">Installing from ArtifactHub</p>
838
894
  <p className="text-xs text-theme-text-secondary mt-1">
839
- This chart will be installed from: <code className="bg-theme-elevated px-1 rounded">{artifactHubRepoUrl || repo}</code>
895
+ This chart will be installed from: <code className="inline-code">{artifactHubRepoUrl || repo}</code>
840
896
  </p>
841
897
  </div>
842
898
  </div>
@@ -873,7 +929,7 @@ function ReviewStep({
873
929
  <>Local: {repo}</>
874
930
  ) : (
875
931
  <>
876
- <Globe className="w-3.5 h-3.5 text-blue-400" />
932
+ <Globe className="w-3.5 h-3.5 text-accent" />
877
933
  ArtifactHub: {repo}
878
934
  </>
879
935
  )}
@@ -1,6 +1,5 @@
1
1
  import { X, GitCompare } from 'lucide-react'
2
- import { PaneLoader } from '@skyhook-io/k8s-ui'
3
- import { clsx } from 'clsx'
2
+ import { PaneLoader, DiffLine, hasDiffBodyChange } from '@skyhook-io/k8s-ui'
4
3
 
5
4
  interface ManifestDiffViewerProps {
6
5
  diff: string
@@ -8,19 +7,21 @@ interface ManifestDiffViewerProps {
8
7
  revision1: number
9
8
  revision2: number
10
9
  onClose: () => void
10
+ title?: string
11
+ emptyLabel?: string
11
12
  }
12
13
 
13
- export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onClose }: ManifestDiffViewerProps) {
14
+ export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onClose, title, emptyLabel }: ManifestDiffViewerProps) {
14
15
  if (isLoading) {
15
16
  return <PaneLoader label="Computing diff…" className="h-32" />
16
17
  }
17
18
 
18
- if (!diff) {
19
+ if (!hasDiffBodyChange(diff)) {
19
20
  return (
20
21
  <div className="p-4">
21
22
  <div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
22
23
  <GitCompare className="w-8 h-8 text-theme-text-disabled" />
23
- <span>No differences found</span>
24
+ <span>{emptyLabel || 'No differences found'}</span>
24
25
  </div>
25
26
  </div>
26
27
  )
@@ -32,7 +33,7 @@ export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onCl
32
33
  <div className="flex items-center gap-2">
33
34
  <GitCompare className="w-4 h-4 text-theme-text-secondary" />
34
35
  <span className="text-sm font-medium text-theme-text-secondary">
35
- Comparing Revision {revision1} → {revision2}
36
+ {title || `Comparing Revision ${revision1} → ${revision2}`}
36
37
  </span>
37
38
  </div>
38
39
  <button
@@ -44,7 +45,7 @@ export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onCl
44
45
  </button>
45
46
  </div>
46
47
 
47
- <div className="rounded-lg overflow-hidden max-h-[calc(100vh-300px)] overflow-auto bg-theme-base/50 font-mono text-xs">
48
+ <div className="rounded-lg max-h-[calc(100vh-300px)] overflow-auto bg-theme-base/50 font-mono text-xs">
48
49
  <div className="p-3">
49
50
  {diff.split('\n').map((line, index) => (
50
51
  <DiffLine key={index} line={line} />
@@ -66,23 +67,3 @@ export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onCl
66
67
  </div>
67
68
  )
68
69
  }
69
-
70
- function DiffLine({ line }: { line: string }) {
71
- const isAddition = line.startsWith('+') && !line.startsWith('+++')
72
- const isRemoval = line.startsWith('-') && !line.startsWith('---')
73
- const isHeader = line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')
74
-
75
- return (
76
- <div
77
- className={clsx(
78
- 'whitespace-pre',
79
- isAddition && 'bg-green-500/10 text-green-400',
80
- isRemoval && 'bg-red-500/10 text-red-400',
81
- isHeader && 'text-theme-text-tertiary font-bold',
82
- !isAddition && !isRemoval && !isHeader && 'text-theme-text-secondary'
83
- )}
84
- >
85
- {line || ' '}
86
- </div>
87
- )
88
- }
@@ -1,18 +1,19 @@
1
1
  import { useState, useCallback } from 'react'
2
- import { Link2, ExternalLink, AlertCircle, Terminal, FileText, Plug, X, Loader2 } from 'lucide-react'
2
+ import { Link2, ExternalLink, AlertCircle, Terminal, FileText, X, Loader2 } from 'lucide-react'
3
3
  import { getResourceIcon } from '../../utils/resource-icons'
4
4
  import { clsx } from 'clsx'
5
5
  import type { HelmOwnedResource } from '../../types'
6
6
  import type { NavigateToResource } from '../../utils/navigation'
7
- import { kindToPlural } from '../../utils/navigation'
7
+ import { kindToPlural, apiVersionToGroup } from '../../utils/navigation'
8
8
  import { getResourceStatusColor, SEVERITY_BADGE } from '../../utils/badge-colors'
9
9
  import { useQueryClient } from '@tanstack/react-query'
10
10
  import { useOpenTerminal, useOpenLogs } from '../dock'
11
- import { useStartPortForward } from '../portforward/PortForwardManager'
11
+ import { PortForwardInlineButton } from '../portforward/PortForwardButton'
12
12
  import { useAvailablePorts } from '../../api/client'
13
13
  import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
14
- import { useNamespacedCapabilities } from '../../contexts/CapabilitiesContext'
14
+ import { useNamespacedCapabilities, useIsLocalDeployment } from '../../contexts/CapabilitiesContext'
15
15
  import { pluralize } from '@skyhook-io/k8s-ui'
16
+ import { Tooltip } from '../ui/Tooltip'
16
17
 
17
18
  interface OwnedResourcesProps {
18
19
  resources: HelmOwnedResource[]
@@ -103,13 +104,14 @@ export function OwnedResources({ resources, onNavigate }: OwnedResourcesProps) {
103
104
  ) : (
104
105
  <span className="flex items-center gap-2">
105
106
  Showing {filteredResources.length} of {resources.length} resources
107
+ <Tooltip content="Clear filter">
106
108
  <button
107
109
  onClick={() => setHealthFilter('all')}
108
110
  className="p-0.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
109
- title="Clear filter"
110
111
  >
111
112
  <X className="w-3.5 h-3.5" />
112
113
  </button>
114
+ </Tooltip>
113
115
  </span>
114
116
  )}
115
117
  </div>
@@ -205,7 +207,7 @@ function ResourceItem({ resource, onNavigate }: ResourceItemProps) {
205
207
 
206
208
  const handleClick = () => {
207
209
  if (onNavigate) {
208
- onNavigate({ kind: kindToPlural(resource.kind), namespace: resource.namespace, name: resource.name })
210
+ onNavigate({ kind: kindToPlural(resource.kind), namespace: resource.namespace, name: resource.name, group: apiVersionToGroup(resource.apiVersion) })
209
211
  }
210
212
  }
211
213
 
@@ -255,39 +257,44 @@ function ResourceItem({ resource, onNavigate }: ResourceItemProps) {
255
257
 
256
258
  {/* Status badge */}
257
259
  {resource.status && (
260
+ <Tooltip content={resource.message || resource.status}>
258
261
  <span
259
262
  className={clsx('badge-sm', getResourceStatusColor(resource.status || ''))}
260
- title={resource.message || resource.status}
261
263
  >
262
264
  {resource.status}
263
265
  </span>
266
+ </Tooltip>
264
267
  )}
265
268
 
266
269
  {/* Issue summary (e.g., "OOMKilled", "CrashLoopBackOff") */}
267
270
  {resource.issue && (
271
+ <Tooltip content={resource.summary || resource.issue}>
268
272
  <span
269
273
  className="text-xs text-red-400"
270
- title={resource.summary || resource.issue}
271
274
  >
272
275
  {resource.issue}
273
276
  </span>
277
+ </Tooltip>
274
278
  )}
275
279
 
276
280
  {/* Error icon with message tooltip */}
277
281
  {isError && resource.message && !resource.issue && (
278
- <span title={resource.message}>
282
+ <Tooltip content={resource.message}>
283
+ <span>
279
284
  <AlertCircle className="w-3.5 h-3.5 text-red-400" />
280
285
  </span>
286
+ </Tooltip>
281
287
  )}
282
288
 
283
289
  {canNavigate && (
290
+ <Tooltip content="View details">
284
291
  <button
285
292
  onClick={handleClick}
286
293
  className="p-1 text-theme-text-tertiary opacity-0 group-hover:opacity-100 hover:text-theme-text-primary hover:bg-theme-elevated rounded transition-all"
287
- title="View details"
288
294
  >
289
295
  <ExternalLink className="w-3.5 h-3.5" />
290
296
  </button>
297
+ </Tooltip>
291
298
  )}
292
299
  </div>
293
300
  </div>
@@ -305,11 +312,14 @@ function PodQuickActions({ namespace, podName, isRunning }: PodQuickActionsProps
305
312
  const queryClient = useQueryClient()
306
313
  const openTerminal = useOpenTerminal()
307
314
  const openLogs = useOpenLogs()
308
- const startPortForward = useStartPortForward()
309
315
  const { data: portsData, isLoading: portsLoading } = useAvailablePorts('pod', namespace, podName)
310
316
 
311
317
  // Capabilities (namespace-scoped: re-checks RBAC if globally denied)
312
318
  const { canExec, canViewLogs, canPortForward } = useNamespacedCapabilities(namespace)
319
+ // Live forward (local + RBAC) or the kubectl copy-command (in-cluster/Cloud);
320
+ // PortForwardInlineButton picks which by deployment mode.
321
+ const isLocal = useIsLocalDeployment()
322
+ const showPortForward = canPortForward || !isLocal
313
323
 
314
324
  const [isLoadingAction, setIsLoadingAction] = useState(false)
315
325
 
@@ -367,14 +377,6 @@ function PodQuickActions({ namespace, podName, isRunning }: PodQuickActionsProps
367
377
  }
368
378
  }, [namespace, podName, openLogs, fetchPodData])
369
379
 
370
- const handlePortForward = useCallback((port: number) => {
371
- startPortForward.mutate({
372
- namespace,
373
- podName,
374
- podPort: port,
375
- })
376
- }, [namespace, podName, startPortForward])
377
-
378
380
  const ports = portsData?.ports || []
379
381
 
380
382
  return (
@@ -383,42 +385,33 @@ function PodQuickActions({ namespace, podName, isRunning }: PodQuickActionsProps
383
385
 
384
386
  {/* Terminal */}
385
387
  {isRunning && canExec && (
388
+ <Tooltip content="Open terminal">
386
389
  <button
387
390
  onClick={(e) => { e.stopPropagation(); handleOpenTerminal() }}
388
391
  disabled={isLoadingAction}
389
- className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
390
- title="Open terminal"
392
+ className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:pointer-events-none"
391
393
  >
392
394
  <Terminal className="w-3.5 h-3.5" />
393
395
  </button>
396
+ </Tooltip>
394
397
  )}
395
398
 
396
399
  {/* Logs */}
397
400
  {canViewLogs && (
401
+ <Tooltip content="View logs">
398
402
  <button
399
403
  onClick={(e) => { e.stopPropagation(); handleOpenLogs() }}
400
404
  disabled={isLoadingAction}
401
- className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
402
- title="View logs"
405
+ className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:pointer-events-none"
403
406
  >
404
407
  <FileText className="w-3.5 h-3.5" />
405
408
  </button>
409
+ </Tooltip>
406
410
  )}
407
411
 
408
- {/* Port Forward */}
409
- {canPortForward && !portsLoading && ports.length > 0 && (
410
- <button
411
- onClick={(e) => { e.stopPropagation(); handlePortForward(ports[0].port) }}
412
- disabled={startPortForward.isPending}
413
- className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
414
- title={`Port forward :${ports[0].port}`}
415
- >
416
- {startPortForward.isPending ? (
417
- <Loader2 className="w-3.5 h-3.5 animate-spin" />
418
- ) : (
419
- <Plug className="w-3.5 h-3.5" />
420
- )}
421
- </button>
412
+ {/* Port Forward (live locally; kubectl copy-command in-cluster/Cloud) */}
413
+ {showPortForward && !portsLoading && ports.length > 0 && (
414
+ <PortForwardInlineButton namespace={namespace} podName={podName} port={ports[0].port} />
422
415
  )}
423
416
  </div>
424
417
  )
@@ -431,36 +424,18 @@ interface ServiceQuickActionsProps {
431
424
  }
432
425
 
433
426
  function ServiceQuickActions({ namespace, serviceName }: ServiceQuickActionsProps) {
434
- const startPortForward = useStartPortForward()
435
427
  const { data: portsData, isLoading: portsLoading } = useAvailablePorts('service', namespace, serviceName)
436
428
  const { canPortForward } = useNamespacedCapabilities(namespace)
437
-
438
- const handlePortForward = useCallback((port: number) => {
439
- startPortForward.mutate({
440
- namespace,
441
- serviceName,
442
- podPort: port,
443
- })
444
- }, [namespace, serviceName, startPortForward])
429
+ const isLocal = useIsLocalDeployment()
430
+ const showPortForward = canPortForward || !isLocal
445
431
 
446
432
  const ports = portsData?.ports || []
447
433
 
448
- if (!canPortForward || portsLoading || ports.length === 0) return null
434
+ if (!showPortForward || portsLoading || ports.length === 0) return null
449
435
 
450
436
  return (
451
437
  <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
452
- <button
453
- onClick={(e) => { e.stopPropagation(); handlePortForward(ports[0].port) }}
454
- disabled={startPortForward.isPending}
455
- className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
456
- title={`Port forward :${ports[0].port}`}
457
- >
458
- {startPortForward.isPending ? (
459
- <Loader2 className="w-3.5 h-3.5 animate-spin" />
460
- ) : (
461
- <Plug className="w-3.5 h-3.5" />
462
- )}
463
- </button>
438
+ <PortForwardInlineButton namespace={namespace} serviceName={serviceName} port={ports[0].port} />
464
439
  </div>
465
440
  )
466
441
  }