@skyhook-io/k8s-ui 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,7 @@ import {
17
17
  import { createTwoFilesPatch } from 'diff'
18
18
  import { clsx } from 'clsx'
19
19
  import { ForceDeleteConfirmDialog } from '../ui/ForceDeleteConfirmDialog'
20
+ import { DialogPortal } from '../ui/DialogPortal'
20
21
  import type { SelectedResource, WorkloadRevision } from '../../types'
21
22
  import { formatKindName } from '../ui/drawer-components'
22
23
 
@@ -619,7 +620,7 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
619
620
  const [confirmRevision, setConfirmRevision] = useState<number | null>(null)
620
621
  const [diffRevision, setDiffRevision] = useState<number | null>(null)
621
622
 
622
- if (!open) return null
623
+ const handleClose = () => { setDiffRevision(null); onClose() }
623
624
 
624
625
  const currentRevision = revisions?.find(r => r.isCurrent)
625
626
  const selectedRevision = revisions?.find(r => r.number === diffRevision)
@@ -660,160 +661,158 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
660
661
  }
661
662
 
662
663
  return (
663
- <div className="fixed inset-0 z-50 flex items-center justify-center">
664
- <div
665
- className="absolute inset-0 bg-black/60 backdrop-blur-sm"
666
- onClick={isRollingBack ? undefined : () => { setDiffRevision(null); onClose() }}
667
- />
668
-
669
- <div className={clsx(
670
- "relative bg-theme-surface border border-theme-border rounded-lg shadow-2xl mx-4 outline-none flex flex-col",
664
+ <DialogPortal
665
+ open={open}
666
+ onClose={handleClose}
667
+ closable={!isRollingBack}
668
+ className={clsx(
669
+ "flex flex-col",
671
670
  diffRevision ? "max-w-5xl w-full max-h-[85vh]" : "max-w-lg w-full"
672
- )}>
673
- <div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
674
- <div className="flex items-center gap-2">
675
- <History className="w-5 h-5 text-amber-500" />
676
- <h3 className="text-lg font-semibold text-theme-text-primary">Revision History</h3>
677
- {diffRevision && currentRevision && (
678
- <span className="flex items-center gap-1 ml-2 px-2 py-0.5 text-xs bg-blue-500/15 text-blue-400 rounded">
679
- <GitCompare className="w-3 h-3" />
680
- #{currentRevision.number} vs #{diffRevision}
681
- </span>
682
- )}
683
- </div>
684
- <button
685
- onClick={() => { setDiffRevision(null); onClose() }}
686
- disabled={isRollingBack}
687
- className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50"
688
- >
689
- <X className="w-5 h-5" />
690
- </button>
671
+ )}
672
+ >
673
+ <div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
674
+ <div className="flex items-center gap-2">
675
+ <History className="w-5 h-5 text-amber-500" />
676
+ <h3 className="text-lg font-semibold text-theme-text-primary">Revision History</h3>
677
+ {diffRevision && currentRevision && (
678
+ <span className="flex items-center gap-1 ml-2 px-2 py-0.5 text-xs bg-blue-500/15 text-blue-400 rounded">
679
+ <GitCompare className="w-3 h-3" />
680
+ #{currentRevision.number} vs #{diffRevision}
681
+ </span>
682
+ )}
691
683
  </div>
684
+ <button
685
+ onClick={handleClose}
686
+ disabled={isRollingBack}
687
+ className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50"
688
+ >
689
+ <X className="w-5 h-5" />
690
+ </button>
691
+ </div>
692
692
 
693
- <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
694
- <div className={clsx("p-4 overflow-y-auto", diffRevision ? "max-h-48 shrink-0" : "max-h-80")}>
695
- {isLoading && (
696
- <div className="flex items-center justify-center py-8 text-theme-text-secondary text-sm">
697
- Loading revisions...
698
- </div>
699
- )}
700
-
701
- {error && (
702
- <div className="flex items-center justify-center py-8 text-red-400 text-sm">
703
- Failed to load revisions: {error instanceof Error ? error.message : 'Unknown error'}
704
- </div>
705
- )}
706
-
707
- {revisions && revisions.length === 0 && (
708
- <div className="flex items-center justify-center py-8 text-theme-text-secondary text-sm">
709
- No revisions found
710
- </div>
711
- )}
712
-
713
- {revisions && revisions.length > 0 && (
714
- <table className="w-full text-sm">
715
- <thead>
716
- <tr className="text-theme-text-secondary text-left text-xs uppercase tracking-wider">
717
- <th className="pb-2 pr-3 font-medium">Rev</th>
718
- <th className="pb-2 pr-3 font-medium">Image</th>
719
- <th className="pb-2 pr-3 font-medium">Age</th>
720
- <th className="pb-2 font-medium text-right">Action</th>
721
- </tr>
722
- </thead>
723
- <tbody>
724
- {revisions.map((rev: WorkloadRevision) => (
725
- <tr
726
- key={rev.number}
727
- className={clsx(
728
- "border-t border-theme-border/50",
729
- diffRevision === rev.number && "bg-blue-500/10"
730
- )}
731
- >
732
- <td className="py-2 pr-3 text-theme-text-primary font-mono">
733
- #{rev.number}
734
- </td>
735
- <td className="py-2 pr-3 text-theme-text-secondary font-mono truncate max-w-[180px]" title={rev.image}>
736
- {getImageTag(rev.image)}
737
- </td>
738
- <td className="py-2 pr-3 text-theme-text-secondary whitespace-nowrap">
739
- {formatTimeAgo(rev.createdAt)}
740
- </td>
741
- <td className="py-2 text-right">
742
- <div className="flex items-center gap-1 justify-end">
743
- {!rev.isCurrent && rev.template && currentRevision?.template && (
693
+ <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
694
+ <div className={clsx("p-4 overflow-y-auto", diffRevision ? "max-h-48 shrink-0" : "max-h-80")}>
695
+ {isLoading && (
696
+ <div className="flex items-center justify-center py-8 text-theme-text-secondary text-sm">
697
+ Loading revisions...
698
+ </div>
699
+ )}
700
+
701
+ {error && (
702
+ <div className="flex items-center justify-center py-8 text-red-400 text-sm">
703
+ Failed to load revisions: {error instanceof Error ? error.message : 'Unknown error'}
704
+ </div>
705
+ )}
706
+
707
+ {revisions && revisions.length === 0 && (
708
+ <div className="flex items-center justify-center py-8 text-theme-text-secondary text-sm">
709
+ No revisions found
710
+ </div>
711
+ )}
712
+
713
+ {revisions && revisions.length > 0 && (
714
+ <table className="w-full text-sm">
715
+ <thead>
716
+ <tr className="text-theme-text-secondary text-left text-xs uppercase tracking-wider">
717
+ <th className="pb-2 pr-3 font-medium">Rev</th>
718
+ <th className="pb-2 pr-3 font-medium">Image</th>
719
+ <th className="pb-2 pr-3 font-medium">Age</th>
720
+ <th className="pb-2 font-medium text-right">Action</th>
721
+ </tr>
722
+ </thead>
723
+ <tbody>
724
+ {revisions.map((rev: WorkloadRevision) => (
725
+ <tr
726
+ key={rev.number}
727
+ className={clsx(
728
+ "border-t border-theme-border/50",
729
+ diffRevision === rev.number && "bg-blue-500/10"
730
+ )}
731
+ >
732
+ <td className="py-2 pr-3 text-theme-text-primary font-mono">
733
+ #{rev.number}
734
+ </td>
735
+ <td className="py-2 pr-3 text-theme-text-secondary font-mono truncate max-w-[180px]" title={rev.image}>
736
+ {getImageTag(rev.image)}
737
+ </td>
738
+ <td className="py-2 pr-3 text-theme-text-secondary whitespace-nowrap">
739
+ {formatTimeAgo(rev.createdAt)}
740
+ </td>
741
+ <td className="py-2 text-right">
742
+ <div className="flex items-center gap-1 justify-end">
743
+ {!rev.isCurrent && rev.template && currentRevision?.template && (
744
+ <button
745
+ onClick={() => setDiffRevision(diffRevision === rev.number ? null : rev.number)}
746
+ className={clsx(
747
+ "px-2 py-0.5 text-xs font-medium rounded transition-colors flex items-center gap-1",
748
+ diffRevision === rev.number
749
+ ? "bg-blue-500/20 text-blue-400 border border-blue-400/50"
750
+ : "text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 border border-transparent"
751
+ )}
752
+ title={`Compare with current revision`}
753
+ >
754
+ <GitCompare className="w-3 h-3" />
755
+ Diff
756
+ </button>
757
+ )}
758
+ {rev.isCurrent ? (
759
+ <span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-green-500/20 text-green-400 rounded">
760
+ Current
761
+ </span>
762
+ ) : confirmRevision === rev.number ? (
763
+ <>
744
764
  <button
745
- onClick={() => setDiffRevision(diffRevision === rev.number ? null : rev.number)}
746
- className={clsx(
747
- "px-2 py-0.5 text-xs font-medium rounded transition-colors flex items-center gap-1",
748
- diffRevision === rev.number
749
- ? "bg-blue-500/20 text-blue-400 border border-blue-400/50"
750
- : "text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 border border-transparent"
751
- )}
752
- title={`Compare with current revision`}
765
+ onClick={() => handleRollback(rev.number)}
766
+ disabled={isRollingBack}
767
+ className="px-2 py-0.5 text-xs font-medium text-white bg-amber-600 hover:bg-amber-700 rounded transition-colors disabled:opacity-50"
753
768
  >
754
- <GitCompare className="w-3 h-3" />
755
- Diff
769
+ {isRollingBack ? 'Rolling back...' : 'Confirm'}
756
770
  </button>
757
- )}
758
- {rev.isCurrent ? (
759
- <span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-green-500/20 text-green-400 rounded">
760
- Current
761
- </span>
762
- ) : confirmRevision === rev.number ? (
763
- <>
764
- <button
765
- onClick={() => handleRollback(rev.number)}
766
- disabled={isRollingBack}
767
- className="px-2 py-0.5 text-xs font-medium text-white bg-amber-600 hover:bg-amber-700 rounded transition-colors disabled:opacity-50"
768
- >
769
- {isRollingBack ? 'Rolling back...' : 'Confirm'}
770
- </button>
771
- <button
772
- onClick={() => setConfirmRevision(null)}
773
- disabled={isRollingBack}
774
- className="px-2 py-0.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary rounded transition-colors disabled:opacity-50"
775
- >
776
- Cancel
777
- </button>
778
- </>
779
- ) : (
780
771
  <button
781
- onClick={() => setConfirmRevision(rev.number)}
782
- className="px-2 py-0.5 text-xs font-medium text-amber-400 hover:text-white hover:bg-amber-600 border border-amber-400/50 hover:border-amber-600 rounded transition-colors"
772
+ onClick={() => setConfirmRevision(null)}
773
+ disabled={isRollingBack}
774
+ className="px-2 py-0.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary rounded transition-colors disabled:opacity-50"
783
775
  >
784
- Rollback
776
+ Cancel
785
777
  </button>
786
- )}
787
- </div>
788
- </td>
789
- </tr>
790
- ))}
791
- </tbody>
792
- </table>
793
- )}
794
- </div>
795
-
796
- {diffRevision && hasDiffData && (
797
- <RevisionDiffView
798
- currentTemplate={currentRevision!.template!}
799
- selectedTemplate={selectedRevision!.template!}
800
- currentRevision={currentRevision!.number}
801
- selectedRevision={diffRevision}
802
- />
778
+ </>
779
+ ) : (
780
+ <button
781
+ onClick={() => setConfirmRevision(rev.number)}
782
+ className="px-2 py-0.5 text-xs font-medium text-amber-400 hover:text-white hover:bg-amber-600 border border-amber-400/50 hover:border-amber-600 rounded transition-colors"
783
+ >
784
+ Rollback
785
+ </button>
786
+ )}
787
+ </div>
788
+ </td>
789
+ </tr>
790
+ ))}
791
+ </tbody>
792
+ </table>
803
793
  )}
804
794
  </div>
805
795
 
806
- <div className="flex items-center justify-end p-4 border-t border-theme-border shrink-0">
807
- <button
808
- onClick={() => { setDiffRevision(null); onClose() }}
809
- disabled={isRollingBack}
810
- className="px-4 py-2 text-sm font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors disabled:opacity-50"
811
- >
812
- Close
813
- </button>
814
- </div>
796
+ {diffRevision && hasDiffData && (
797
+ <RevisionDiffView
798
+ currentTemplate={currentRevision!.template!}
799
+ selectedTemplate={selectedRevision!.template!}
800
+ currentRevision={currentRevision!.number}
801
+ selectedRevision={diffRevision}
802
+ />
803
+ )}
815
804
  </div>
816
- </div>
805
+
806
+ <div className="flex items-center justify-end p-4 border-t border-theme-border shrink-0">
807
+ <button
808
+ onClick={handleClose}
809
+ disabled={isRollingBack}
810
+ className="px-4 py-2 text-sm font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors disabled:opacity-50"
811
+ >
812
+ Close
813
+ </button>
814
+ </div>
815
+ </DialogPortal>
817
816
  )
818
817
  }
819
818
 
@@ -171,6 +171,33 @@ import {
171
171
  TraefikIngressRouteRenderer,
172
172
  } from '../resources/renderers'
173
173
  import type { SelectedResource, Relationships, ResourceRef, SecretCertificateInfo } from '../../types'
174
+ import type { CopyHandler } from '../ui/drawer-components'
175
+
176
+ /**
177
+ * Override map letting each platform consumer swap in its own renderer components.
178
+ * Each override receives only the props that ResourceRendererDispatch passes at its
179
+ * call site — a subset of the base renderer's full props. The override is responsible
180
+ * for wiring any additional behavior (metrics, exec, port-forward, scale, etc.) internally.
181
+ *
182
+ * When an override is not provided, the base (shared) renderer is used.
183
+ */
184
+ export interface RendererOverrides {
185
+ PodRenderer?: React.ComponentType<{
186
+ data: any; onCopy: CopyHandler; copied: string | null
187
+ onNavigate?: (ref: ResourceRef) => void
188
+ onOpenLogs?: (podName: string, containerName: string) => void
189
+ }>
190
+ NodeRenderer?: React.ComponentType<{
191
+ data: any; relationships?: Relationships
192
+ }>
193
+ ServiceRenderer?: React.ComponentType<{
194
+ data: any; onCopy: CopyHandler; copied: string | null
195
+ }>
196
+ WorkloadRenderer?: React.ComponentType<{
197
+ kind: string; data: any
198
+ onNavigate?: (ref: ResourceRef) => void
199
+ }>
200
+ }
174
201
 
175
202
  // Known resource types with specific renderers (module-level to avoid re-allocation)
176
203
  const KNOWN_KINDS = new Set([
@@ -238,6 +265,8 @@ interface ResourceRendererDispatchProps {
238
265
  eventsLoading?: boolean
239
266
  /** Render prop for Prometheus metrics charts — injected by the platform wrapper */
240
267
  renderMetrics?: (props: { kind: string; namespace: string; name: string }) => React.ReactNode
268
+ /** Platform-specific renderer overrides (e.g. with hooks for metrics, exec, port-forward) */
269
+ rendererOverrides?: RendererOverrides
241
270
  }
242
271
 
243
272
  export function ResourceRendererDispatch({
@@ -258,11 +287,18 @@ export function ResourceRendererDispatch({
258
287
  events,
259
288
  eventsLoading,
260
289
  renderMetrics,
290
+ rendererOverrides,
261
291
  }: ResourceRendererDispatchProps) {
262
292
  const kind = resource.kind.toLowerCase()
263
293
 
264
294
  const isKnownKind = KNOWN_KINDS.has(kind)
265
295
 
296
+ // Resolve renderer components — use platform override when provided, otherwise base
297
+ const PodComp = rendererOverrides?.PodRenderer ?? PodRenderer
298
+ const WorkloadComp = rendererOverrides?.WorkloadRenderer ?? WorkloadRenderer
299
+ const NodeComp = rendererOverrides?.NodeRenderer ?? NodeRenderer
300
+ const ServiceComp = rendererOverrides?.ServiceRenderer ?? ServiceRenderer
301
+
266
302
  const sidebarContent = showCommonSections && (
267
303
  <>
268
304
  <RelatedResourcesSection relationships={relationships} onNavigate={onNavigate} />
@@ -277,17 +313,17 @@ export function ResourceRendererDispatch({
277
313
  <div className={renderSidebar ? 'lg:flex' : ''}>
278
314
  <div className={clsx('p-4 space-y-4', renderSidebar && 'lg:flex-1 lg:min-w-0')}>
279
315
  {/* Kind-specific content - delegates to modular renderers */}
280
- {kind === 'pods' && <PodRenderer data={data} onCopy={onCopy} copied={copied} onNavigate={onNavigate} onOpenLogs={onOpenLogs} />}
281
- {['deployments', 'statefulsets', 'daemonsets'].includes(kind) && <WorkloadRenderer kind={kind} data={data} onNavigate={onNavigate} />}
316
+ {kind === 'pods' && <PodComp data={data} onCopy={onCopy} copied={copied} onNavigate={onNavigate} onOpenLogs={onOpenLogs} />}
317
+ {['deployments', 'statefulsets', 'daemonsets'].includes(kind) && <WorkloadComp kind={kind} data={data} onNavigate={onNavigate} />}
282
318
  {kind === 'replicasets' && <ReplicaSetRenderer data={data} />}
283
- {kind === 'services' && !data?.apiVersion?.includes('serving.knative.dev') && <ServiceRenderer data={data} onCopy={onCopy} copied={copied} />}
319
+ {kind === 'services' && !data?.apiVersion?.includes('serving.knative.dev') && <ServiceComp data={data} onCopy={onCopy} copied={copied} />}
284
320
  {kind === 'ingresses' && !data?.apiVersion?.includes('networking.internal.knative.dev') && <IngressRenderer data={data} onNavigate={onNavigate} />}
285
321
  {kind === 'configmaps' && <ConfigMapRenderer data={data} />}
286
322
  {kind === 'secrets' && <SecretRenderer data={data} certificateInfo={certificateInfo} resourceData={data} onSaveSecretValue={onSaveSecretValue} isSaving={isSavingSecret} />}
287
323
  {kind === 'jobs' && <JobRenderer data={data} />}
288
324
  {kind === 'cronjobs' && <CronJobRenderer data={data} onNavigate={onNavigate} />}
289
325
  {(kind === 'hpas' || kind === 'horizontalpodautoscalers') && <HPARenderer data={data} onNavigate={onNavigate} />}
290
- {kind === 'nodes' && <NodeRenderer data={data} relationships={relationships} />}
326
+ {kind === 'nodes' && <NodeComp data={data} relationships={relationships} />}
291
327
  {kind === 'persistentvolumeclaims' && <PVCRenderer data={data} onNavigate={onNavigate} />}
292
328
  {kind === 'rollouts' && <RolloutRenderer data={data} />}
293
329
  {kind === 'certificates' && !data?.apiVersion?.includes('networking.internal.knative.dev') && <CertificateRenderer data={data} />}
@@ -1,3 +1,3 @@
1
- export { ResourceRendererDispatch, getResourceStatus } from './ResourceRendererDispatch'
1
+ export { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } from './ResourceRendererDispatch'
2
2
  export { EditableYamlView, SaveSuccessAnimation } from './EditableYamlView'
3
3
  export { ResourceActionsBar, RevisionHistoryDialog } from './ResourceActionsBar'