@skyhook-io/radar-app 1.3.1 → 1.3.3

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/App.tsx +111 -58
  3. package/src/api/client.ts +29 -1
  4. package/src/components/ConnectionErrorView.tsx +2 -2
  5. package/src/components/gitops/GitOpsView.tsx +127 -27
  6. package/src/components/helm/ChartBrowser.tsx +7 -3
  7. package/src/components/helm/HelmReleaseDrawer.tsx +4 -6
  8. package/src/components/helm/InstallWizard.tsx +1 -1
  9. package/src/components/helm/RoleGatedPanel.tsx +2 -2
  10. package/src/components/home/ClusterHealthCard.tsx +1 -1
  11. package/src/components/home/GitOpsControllersCard.tsx +14 -12
  12. package/src/components/home/HomeView.tsx +84 -56
  13. package/src/components/home/MCPSetupDialog.tsx +20 -86
  14. package/src/components/home/mcpToolCatalog.ts +276 -0
  15. package/src/components/issues/IssuesPane.tsx +78 -0
  16. package/src/components/portforward/PortForwardButton.tsx +1 -1
  17. package/src/components/portforward/PortForwardManager.tsx +1 -1
  18. package/src/components/resource/PrometheusCharts.tsx +18 -159
  19. package/src/components/resources/ImageFilesystemModal.tsx +1 -2
  20. package/src/components/resources/renderers/RoleBindingRenderer.tsx +5 -3
  21. package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -2
  22. package/src/components/settings/MyPermissionsDialog.tsx +1 -1
  23. package/src/components/settings/SettingsDialog.tsx +22 -2
  24. package/src/components/timeline/TimelineSwimlanes.tsx +8 -1311
  25. package/src/components/ui/Markdown.tsx +1 -1
  26. package/src/components/ui/UpdateNotification.tsx +1 -1
  27. package/src/components/workload/WorkloadView.tsx +190 -7
@@ -51,7 +51,7 @@ export function Markdown({ children, className }: MarkdownProps) {
51
51
  const isInline = !className
52
52
  if (isInline) {
53
53
  return (
54
- <code className="px-1.5 py-0.5 bg-theme-elevated rounded text-theme-text-primary font-mono text-[0.9em]">
54
+ <code className="inline-code">
55
55
  {children}
56
56
  </code>
57
57
  )
@@ -147,7 +147,7 @@ export function UpdateNotification() {
147
147
  onClick={handleCopyCommand}
148
148
  className="flex items-center gap-2 mt-2 px-2 py-1.5 bg-theme-elevated rounded font-mono text-theme-text-primary hover:bg-theme-surface-hover transition-colors w-full"
149
149
  >
150
- <code className="flex-1 text-left truncate text-[11px]">{versionInfo.updateCommand}</code>
150
+ <code className="inline-code flex-1 truncate text-left text-[11px]">{versionInfo.updateCommand}</code>
151
151
  <CopyIcon copied={copied} failed={copyFailed} />
152
152
  </button>
153
153
  </WithTooltip>
@@ -7,9 +7,14 @@ import {
7
7
  WorkloadView as BaseWorkloadView,
8
8
  type RendererOverrides,
9
9
  type GitOpsOwnerRef,
10
+ type GitOpsStatus,
11
+ type HelmOwnerRef,
10
12
  gitOpsRouteForOwner,
13
+ gitOpsOwnerFromRelationships,
14
+ getGitOpsResourceStatus,
15
+ resolvedEnvFromKey,
11
16
  } from '@skyhook-io/k8s-ui'
12
- import type { SelectedResource, ResourceRef, ResolvedEnvFrom } from '../../types'
17
+ import type { SelectedResource, ResourceRef, Relationships, ResolvedEnvFrom } from '../../types'
13
18
  import { kindToPlural, buildWorkloadPath, type NavigateToResource } from '../../utils/navigation'
14
19
  import {
15
20
  useChanges, useResourceWithRelationships, usePodLogs, useTopology, useUpdateResource,
@@ -20,6 +25,7 @@ import {
20
25
  useCordonNode, useUncordonNode, useDrainNode,
21
26
  useCascadeDeletePreview,
22
27
  useResourceEvents,
28
+ useResource,
23
29
  fetchJSON,
24
30
  } from '../../api/client'
25
31
  import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
@@ -255,10 +261,65 @@ export function WorkloadView({
255
261
  }, [searchParams, setSearchParams])
256
262
 
257
263
  // Fetch resource with relationships
258
- const { data: resourceResponse, isLoading: resourceLoading, refetch: refetchResource } = useResourceWithRelationships<any>(kindProp, namespace, name, rest.group)
264
+ const { data: resourceResponse, isLoading: resourceLoading, error: resourceError, refetch: refetchResource } = useResourceWithRelationships<any>(kindProp, namespace, name, rest.group)
259
265
  const resource = resourceResponse?.resource
260
266
  const relationships = resourceResponse?.relationships
261
267
  const certificateInfo = resourceResponse?.certificateInfo
268
+ const relationshipGitopsOwner = useMemo(() => gitOpsOwnerFromRelationships(relationships), [relationships])
269
+ const inheritedGitOpsLookupRef = useMemo(
270
+ () => findInheritedGitOpsLookupRef(relationships, relationshipGitopsOwner, { kind: kindProp, namespace, name, group: rest.group }),
271
+ [relationships, relationshipGitopsOwner, kindProp, namespace, name, rest.group],
272
+ )
273
+ const inheritedGitOpsResponse = useResourceWithRelationships<any>(
274
+ inheritedGitOpsLookupRef ? kindToPlural(inheritedGitOpsLookupRef.kind) : '',
275
+ inheritedGitOpsLookupRef?.namespace ?? '',
276
+ inheritedGitOpsLookupRef?.name ?? '',
277
+ inheritedGitOpsLookupRef?.group,
278
+ )
279
+ const inheritedGitopsOwner = useMemo(
280
+ () => gitOpsOwnerFromRelationships(inheritedGitOpsResponse.data?.relationships),
281
+ [inheritedGitOpsResponse.data?.relationships],
282
+ )
283
+ const relationshipHelmOwner = useMemo(
284
+ () => nativeHelmOwnerFromRelationships(relationships, resource?.metadata?.namespace ?? namespace),
285
+ [relationships, resource?.metadata?.namespace, namespace],
286
+ )
287
+ const inheritedHelmOwner = useMemo(
288
+ () => nativeHelmOwnerFromRelationships(inheritedGitOpsResponse.data?.relationships, inheritedGitOpsResponse.data?.resource?.metadata?.namespace ?? namespace),
289
+ [inheritedGitOpsResponse.data?.relationships, inheritedGitOpsResponse.data?.resource?.metadata?.namespace, namespace],
290
+ )
291
+ const rawGitopsOwner = relationshipGitopsOwner ?? inheritedGitopsOwner
292
+ const gitOpsSourceResource = relationshipGitopsOwner ? resource : inheritedGitOpsResponse.data?.resource
293
+ const helmOwner = relationshipHelmOwner ?? inheritedHelmOwner
294
+ const helmSourceResource = relationshipHelmOwner ? resource : inheritedGitOpsResponse.data?.resource
295
+ const shouldResolveArgoOwner = rawGitopsOwner?.tool === 'argocd' && !rawGitopsOwner.namespace
296
+ const { data: argoApplications } = useResources<any>('applications', undefined, 'argoproj.io', { enabled: shouldResolveArgoOwner })
297
+ const gitopsOwner = useMemo(
298
+ () => resolveGitOpsOwner(rawGitopsOwner, argoApplications),
299
+ [rawGitopsOwner, argoApplications],
300
+ )
301
+ const gitopsOwnerGroup = gitopsOwner ? gitOpsOwnerGroup(gitopsOwner) : ''
302
+ const shouldFetchGitOpsOwner = Boolean(gitopsOwner?.namespace)
303
+ const gitopsOwnerQuery = useResource<any>(
304
+ shouldFetchGitOpsOwner ? gitopsOwner!.kind : '',
305
+ gitopsOwner?.namespace ?? '',
306
+ gitopsOwner?.name ?? '',
307
+ gitopsOwnerGroup,
308
+ )
309
+ const gitOpsOwnerStatus = useMemo(
310
+ () => deriveGitOpsOwnerStatus(gitopsOwner, gitopsOwnerQuery.data),
311
+ [gitopsOwner, gitopsOwnerQuery.data],
312
+ )
313
+ const gitOpsOwnerVerified = Boolean(gitopsOwner?.namespace && gitopsOwnerQuery.data)
314
+ const gitOpsOwnerPending = Boolean(gitopsOwner?.namespace && gitopsOwnerQuery.isLoading && !gitopsOwnerQuery.data)
315
+ const gitOpsOwnerSource = useMemo(
316
+ () => describeGitOpsOwnerSource(rawGitopsOwner, gitOpsSourceResource),
317
+ [rawGitopsOwner, gitOpsSourceResource],
318
+ )
319
+ const helmOwnerSource = useMemo(
320
+ () => describeHelmOwnerSource(helmOwner, helmSourceResource),
321
+ [helmOwner, helmSourceResource],
322
+ )
262
323
 
263
324
  // For pods: extract envFrom ConfigMap/Secret names and resolve their keys
264
325
  const isPod = kindProp.toLowerCase() === 'pods'
@@ -300,7 +361,7 @@ export function WorkloadView({
300
361
  envFromConfigMapNames.forEach((n, i) => {
301
362
  // Single-resource endpoint returns { resource, relationships } wrapper
302
363
  const cm = configMapQueries[i]?.data?.resource ?? configMapQueries[i]?.data
303
- if (cm) result[n] = { keys: Object.keys(cm.data || {}), values: cm.data || {}, isSecret: false }
364
+ if (cm) result[resolvedEnvFromKey('configmap', n)] = { keys: Object.keys(cm.data || {}), values: cm.data || {}, isSecret: false }
304
365
  })
305
366
  envFromSecretNames.forEach((n, i) => {
306
367
  const secret = secretQueries[i]?.data?.resource ?? secretQueries[i]?.data
@@ -309,7 +370,7 @@ export function WorkloadView({
309
370
  for (const [k, v] of Object.entries(secret.data || {})) {
310
371
  try { decodedValues[k] = atob(v as string) } catch { decodedValues[k] = v as string }
311
372
  }
312
- result[n] = { keys: Object.keys(decodedValues), values: decodedValues, isSecret: true }
373
+ result[resolvedEnvFromKey('secret', n)] = { keys: Object.keys(decodedValues), values: decodedValues, isSecret: true }
313
374
  }
314
375
  })
315
376
  return Object.keys(result).length > 0 ? result : undefined
@@ -368,13 +429,28 @@ export function WorkloadView({
368
429
 
369
430
  const navigateRouter = useNavigate()
370
431
  const handleOpenGitOpsResource = useCallback(
371
- (ref: GitOpsOwnerRef) => navigateRouter(gitOpsRouteForOwner(ref)),
372
- [navigateRouter],
432
+ (ref: GitOpsOwnerRef) => {
433
+ const params = new URLSearchParams()
434
+ const namespaces = searchParams.get('namespaces')
435
+ if (namespaces) params.set('namespaces', namespaces)
436
+ navigateRouter({ pathname: gitOpsRouteForOwner(ref), search: params.toString() })
437
+ },
438
+ [navigateRouter, searchParams],
373
439
  )
374
440
  const handleNavigateGitOpsPath = useCallback(
375
441
  (path: string) => navigateRouter(path),
376
442
  [navigateRouter],
377
443
  )
444
+ const handleOpenHelmRelease = useCallback(
445
+ (ref: HelmOwnerRef) => {
446
+ const params = new URLSearchParams()
447
+ const namespaces = searchParams.get('namespaces')
448
+ if (namespaces) params.set('namespaces', namespaces)
449
+ params.set('release', `${ref.namespace}/${ref.name}`)
450
+ navigateRouter({ pathname: '/helm', search: params.toString() })
451
+ },
452
+ [navigateRouter, searchParams],
453
+ )
378
454
 
379
455
  // Duplicate dialog
380
456
  const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
@@ -398,6 +474,7 @@ export function WorkloadView({
398
474
  relationships={relationships}
399
475
  certificateInfo={certificateInfo}
400
476
  isLoading={resourceLoading}
477
+ resourceError={resourceError}
401
478
  refetch={refetchResource}
402
479
  // Timeline
403
480
  allEvents={allEvents}
@@ -436,7 +513,15 @@ export function WorkloadView({
436
513
  <FluxSourceConsumersSection kind={k} namespace={ns} name={n} />
437
514
  </>
438
515
  )}
439
- onOpenGitOpsResource={handleOpenGitOpsResource}
516
+ onOpenGitOpsResource={gitopsOwnerQuery.data ? handleOpenGitOpsResource : undefined}
517
+ resolvedGitOpsOwner={gitopsOwner}
518
+ gitOpsOwnerVerified={gitOpsOwnerVerified}
519
+ gitOpsOwnerPending={gitOpsOwnerPending}
520
+ gitOpsOwnerSource={gitOpsOwnerSource}
521
+ gitOpsOwnerStatus={gitOpsOwnerStatus}
522
+ helmOwner={helmOwner}
523
+ helmOwnerSource={helmOwnerSource}
524
+ onOpenHelmRelease={handleOpenHelmRelease}
440
525
  onNavigateGitOpsPath={handleNavigateGitOpsPath}
441
526
  />
442
527
  <CreateResourceDialog
@@ -453,6 +538,104 @@ export function WorkloadView({
453
538
  )
454
539
  }
455
540
 
541
+ function resolveGitOpsOwner(owner: GitOpsOwnerRef | null, argoApplications: any[] | undefined): GitOpsOwnerRef | null {
542
+ if (!owner || owner.namespace || owner.tool !== 'argocd') return owner
543
+ const matches = (argoApplications ?? []).filter((app) => app?.metadata?.name === owner.name)
544
+ if (matches.length !== 1) return owner
545
+ const namespace = matches[0]?.metadata?.namespace
546
+ return namespace ? { ...owner, namespace } : owner
547
+ }
548
+
549
+ function findInheritedGitOpsLookupRef(
550
+ relationships: Relationships | undefined,
551
+ directOwner: GitOpsOwnerRef | null,
552
+ current: ResourceRef,
553
+ ): ResourceRef | null {
554
+ if (directOwner) return null
555
+ const inheritedManagerRefs = (relationships?.managedBy ?? []).filter((ref) =>
556
+ !gitOpsOwnerFromRelationships({ managedBy: [ref] })
557
+ && !isNativeHelmManager(ref)
558
+ )
559
+ const candidates = [
560
+ relationships?.deployment,
561
+ ...inheritedManagerRefs,
562
+ relationships?.owner,
563
+ ].filter(Boolean) as ResourceRef[]
564
+
565
+ return candidates.find((ref) => !isCurrentResource(ref, current)) ?? null
566
+ }
567
+
568
+ function nativeHelmOwnerFromRelationships(relationships: Relationships | undefined, fallbackNamespace: string): HelmOwnerRef | null {
569
+ const ref = relationships?.managedBy?.[0]
570
+ if (!ref || !isNativeHelmManager(ref)) return null
571
+ return {
572
+ namespace: ref.namespace || fallbackNamespace,
573
+ name: ref.name,
574
+ }
575
+ }
576
+
577
+ function isCurrentResource(ref: ResourceRef, current: ResourceRef): boolean {
578
+ return kindToPlural(ref.kind) === kindToPlural(current.kind)
579
+ && ref.namespace === current.namespace
580
+ && ref.name === current.name
581
+ && (ref.group ?? '') === (current.group ?? '')
582
+ }
583
+
584
+ function isNativeHelmManager(ref: ResourceRef): boolean {
585
+ return ref.kind === 'HelmRelease' && ref.group !== 'helm.toolkit.fluxcd.io'
586
+ }
587
+
588
+ function describeGitOpsOwnerSource(owner: GitOpsOwnerRef | null, resource: any): string | null {
589
+ if (!owner || !resource) return null
590
+ const labels = resource.metadata?.labels ?? {}
591
+ const annotations = resource.metadata?.annotations ?? {}
592
+
593
+ if (owner.tool === 'fluxcd') {
594
+ const nameKey = owner.kind === 'helmreleases' ? 'helm.toolkit.fluxcd.io/name' : 'kustomize.toolkit.fluxcd.io/name'
595
+ const nsKey = owner.kind === 'helmreleases' ? 'helm.toolkit.fluxcd.io/namespace' : 'kustomize.toolkit.fluxcd.io/namespace'
596
+ if (labels[nameKey] || labels[nsKey]) {
597
+ return `${nameKey}=${labels[nameKey] ?? ''}, ${nsKey}=${labels[nsKey] ?? ''}`
598
+ }
599
+ }
600
+
601
+ const trackingID = annotations['argocd.argoproj.io/tracking-id']
602
+ if (trackingID) return `argocd.argoproj.io/tracking-id=${trackingID}`
603
+ const argoInstance = labels['argocd.argoproj.io/instance']
604
+ if (argoInstance) return `argocd.argoproj.io/instance=${argoInstance}`
605
+ return null
606
+ }
607
+
608
+ function describeHelmOwnerSource(owner: HelmOwnerRef | null, resource: any): string | null {
609
+ if (!owner || !resource) return null
610
+ const annotations = resource.metadata?.annotations ?? {}
611
+ const releaseName = annotations['meta.helm.sh/release-name']
612
+ const releaseNamespace = annotations['meta.helm.sh/release-namespace']
613
+ if (releaseName || releaseNamespace) {
614
+ return `meta.helm.sh/release-name=${releaseName ?? ''}, meta.helm.sh/release-namespace=${releaseNamespace ?? ''}`
615
+ }
616
+ return null
617
+ }
618
+
619
+ function gitOpsOwnerGroup(owner: GitOpsOwnerRef): string {
620
+ if (owner.tool === 'argocd') return 'argoproj.io'
621
+ if (owner.kind === 'kustomizations') return 'kustomize.toolkit.fluxcd.io'
622
+ return 'helm.toolkit.fluxcd.io'
623
+ }
624
+
625
+ function deriveGitOpsOwnerStatus(owner: GitOpsOwnerRef | null, resource: any): GitOpsStatus | null {
626
+ if (!owner || !resource || !hasGitOpsStatusPayload(owner, resource)) return null
627
+ return getGitOpsResourceStatus(owner.kind, resource)
628
+ }
629
+
630
+ function hasGitOpsStatusPayload(owner: GitOpsOwnerRef, resource: any): boolean {
631
+ if (owner.kind === 'applications') {
632
+ const status = resource.status ?? {}
633
+ return Boolean(status.sync?.status || status.health?.status || status.operationState?.phase)
634
+ }
635
+ if (resource.spec?.suspend === true) return true
636
+ return Array.isArray(resource.status?.conditions) && resource.status.conditions.length > 0
637
+ }
638
+
456
639
  // ============================================================================
457
640
  // LOGS TAB — platform-specific (uses data-fetching hooks)
458
641
  // ============================================================================