@skyhook-io/k8s-ui 1.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "scripts": {
25
25
  "tsc": "tsc --noEmit",
26
+ "test": "vitest run",
26
27
  "format": "prettier --write \"src/**/*.{ts,tsx}\""
27
28
  },
28
29
  "dependencies": {
@@ -59,6 +60,7 @@
59
60
  "react": "^19.2.4",
60
61
  "react-dom": "^19.2.4",
61
62
  "typescript": "^5.9.3",
63
+ "vitest": "^4.0.18",
62
64
  "yaml": "^2.7.1"
63
65
  }
64
66
  }
@@ -1598,8 +1598,6 @@ export function ResourcesView({
1598
1598
  // Pinned kinds (favorites) — provided via props
1599
1599
  const [favoritesExpanded, setFavoritesExpanded] = useState(() => pinned.length > 0)
1600
1600
 
1601
- console.debug('[filters] ResourcesView render:', { kind: selectedKind.name, columnFilters, searchTerm, url: location.search })
1602
-
1603
1601
  useEffect(() => { persistedExpandedCategories = expandedCategories }, [expandedCategories])
1604
1602
  // Track if this is the initial mount to avoid re-syncing on first render
1605
1603
  const isInitialMount = useRef(true)
@@ -2198,6 +2196,14 @@ export function ResourcesView({
2198
2196
  }
2199
2197
  // Signal that initial resource param has been processed — URL update effect can now run
2200
2198
  hasProcessedInitialResource.current = true
2199
+
2200
+ // If the URL has no kind segment (e.g., /resources), update to include the default kind
2201
+ const base = basePath.replace(/\/$/, '')
2202
+ const path = window.location.pathname
2203
+ if (path === base || path === base + '/') {
2204
+ const search = window.location.search
2205
+ window.history.replaceState({}, '', `${base}/${selectedKind.name}${search}`)
2206
+ }
2201
2207
  }, []) // Only on mount
2202
2208
 
2203
2209
  // API resources for dynamic sidebar — provided via props
@@ -3041,18 +3047,16 @@ export function ResourcesView({
3041
3047
  <ResourcesViewDataContext.Provider value={resourcesViewDataContextValue}>
3042
3048
  <div className="flex h-full w-full">
3043
3049
  {/* Sidebar - Resource Types */}
3044
- <div className="w-72 bg-theme-surface border-r border-theme-border overflow-y-auto overflow-x-hidden shrink-0">
3045
- <div className="flex items-center gap-2 px-3 py-3 border-b border-theme-border">
3046
- <h2 className="text-sm font-medium text-theme-text-secondary uppercase tracking-wide shrink-0">
3047
- Resources
3048
- </h2>
3049
- <div className="flex-1 relative">
3050
+ <div className="w-56 2xl:w-72 bg-theme-surface border-r border-theme-border overflow-y-auto overflow-x-hidden shrink-0">
3051
+ <div className="px-2 py-2 border-b border-theme-border">
3052
+ <div className="relative">
3050
3053
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
3051
3054
  <input
3052
3055
  type="text"
3053
- placeholder="Filter kinds..."
3056
+ placeholder="Filter resources..."
3054
3057
  value={kindFilter}
3055
3058
  onChange={(e) => setKindFilter(e.target.value)}
3059
+ onKeyDown={(e) => { if (e.key === 'Escape') { setKindFilter(''); (e.target as HTMLInputElement).blur() } }}
3056
3060
  className="w-full pl-7 pr-7 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"
3057
3061
  />
3058
3062
  {kindFilter && (
@@ -3079,7 +3083,7 @@ export function ResourcesView({
3079
3083
  )}
3080
3084
  <span className="flex-1 text-left">Favorites</span>
3081
3085
  {!favoritesExpanded && pinned.length > 0 && (
3082
- <span className="text-xs px-1.5 py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case">
3086
+ <span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center', pinned.length < 1000 ? 'w-8' : 'w-9')}>
3083
3087
  {pinned.length}
3084
3088
  </span>
3085
3089
  )}
@@ -3125,7 +3129,7 @@ export function ResourcesView({
3125
3129
  <div key={category.name} className="mb-2">
3126
3130
  <button
3127
3131
  onClick={() => toggleCategory(category.name)}
3128
- className="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary uppercase tracking-wide"
3132
+ className="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-semibold text-theme-text-tertiary hover:text-theme-text-secondary uppercase tracking-wide"
3129
3133
  >
3130
3134
  {isExpanded ? (
3131
3135
  <ChevronDown className="w-3 h-3" />
@@ -3134,7 +3138,7 @@ export function ResourcesView({
3134
3138
  )}
3135
3139
  <span className="flex-1 text-left">{category.name}</span>
3136
3140
  {!isExpanded && (
3137
- <span className="text-xs px-1.5 py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case">
3141
+ <span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center', category.total < 1000 ? 'w-8' : 'w-9')}>
3138
3142
  {category.total}
3139
3143
  </span>
3140
3144
  )}
@@ -3781,7 +3785,7 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
3781
3785
  ref={ref}
3782
3786
  onClick={onClick}
3783
3787
  className={clsx(
3784
- 'w-full flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors group/kind min-w-0',
3788
+ 'w-full flex items-center gap-2 px-2 xl:px-3 py-1.5 rounded-lg text-sm transition-colors group/kind min-w-0',
3785
3789
  isSelected
3786
3790
  ? 'bg-blue-500/20 text-blue-700 dark:text-blue-300'
3787
3791
  : forbidden
@@ -3795,36 +3799,39 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
3795
3799
  {resource.kind}
3796
3800
  </span>
3797
3801
  </Tooltip>
3798
- {forbidden ? (
3799
- <Tooltip content="Insufficient permissions" position="left">
3800
- <Shield className="w-3.5 h-3.5 text-amber-400/60" />
3801
- </Tooltip>
3802
- ) : (
3803
- <span className={clsx(
3804
- 'text-xs px-1.5 py-0.5 rounded min-w-[1.5rem] text-center',
3805
- isSelected ? 'bg-blue-500/30 text-blue-700 dark:text-blue-300' : 'bg-theme-elevated'
3806
- )}>
3807
- {count}
3808
- </span>
3809
- )}
3810
- {onTogglePin && (
3811
- <span
3812
- role="button"
3813
- onClick={(e) => {
3814
- e.stopPropagation()
3815
- onTogglePin()
3816
- }}
3817
- className={clsx(
3818
- 'ml-auto p-0.5 rounded transition-all shrink-0 hover:bg-theme-hover',
3819
- isPinned
3820
- ? 'text-theme-text-secondary'
3821
- : 'opacity-0 group-hover/kind:opacity-100 text-theme-text-disabled'
3822
- )}
3823
- title={isPinned ? 'Unpin from favorites' : 'Pin to favorites'}
3824
- >
3825
- <Pin className={clsx('w-3.5 h-3.5', isPinned && 'fill-current')} />
3826
- </span>
3827
- )}
3802
+ <div className="ml-auto flex items-center gap-1 shrink-0">
3803
+ {onTogglePin && (
3804
+ <span
3805
+ role="button"
3806
+ onClick={(e) => {
3807
+ e.stopPropagation()
3808
+ onTogglePin()
3809
+ }}
3810
+ className={clsx(
3811
+ 'p-0.5 rounded transition-all hover:bg-theme-hover',
3812
+ isPinned
3813
+ ? 'text-theme-text-secondary'
3814
+ : 'opacity-0 group-hover/kind:opacity-100 text-theme-text-disabled'
3815
+ )}
3816
+ title={isPinned ? 'Unpin from favorites' : 'Pin to favorites'}
3817
+ >
3818
+ <Pin className={clsx('w-3 h-3', isPinned && 'fill-current')} />
3819
+ </span>
3820
+ )}
3821
+ {forbidden ? (
3822
+ <Tooltip content="Insufficient permissions" position="left">
3823
+ <Shield className="w-3.5 h-3.5 text-amber-400/60" />
3824
+ </Tooltip>
3825
+ ) : (
3826
+ <span className={clsx(
3827
+ 'text-xs py-0.5 rounded text-center',
3828
+ isSelected ? 'bg-blue-500/30 text-blue-700 dark:text-blue-300' : 'bg-theme-elevated',
3829
+ count < 1000 ? 'w-8' : 'w-9'
3830
+ )}>
3831
+ {count}
3832
+ </span>
3833
+ )}
3834
+ </div>
3828
3835
  </button>
3829
3836
  )
3830
3837
  }
@@ -1,6 +1,7 @@
1
1
  import { Cpu, AlertTriangle } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, ResourceLink } from '../../ui/drawer-components'
4
+ import { kindToPlural } from '../../../utils/navigation'
4
5
  import { formatAge } from '../resource-utils'
5
6
 
6
7
  interface HPARendererProps {
@@ -113,7 +114,7 @@ export function HPARenderer({ data, onNavigate }: HPARendererProps) {
113
114
  spec.scaleTargetRef?.name ? (
114
115
  <ResourceLink
115
116
  name={spec.scaleTargetRef.name}
116
- kind={(spec.scaleTargetRef.kind || 'Deployment').toLowerCase() + 's'}
117
+ kind={kindToPlural(spec.scaleTargetRef.kind || 'Deployment')}
117
118
  namespace={data.metadata?.namespace || ''}
118
119
  label={`${spec.scaleTargetRef.kind}/${spec.scaleTargetRef.name}`}
119
120
  onNavigate={onNavigate}
@@ -1,6 +1,7 @@
1
1
  import { Server, Cpu, Settings } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { kindToPlural } from '../../../utils/navigation'
4
5
  import {
5
6
  getNodeClaimStatus,
6
7
  getNodeClaimInstanceType,
@@ -73,7 +74,7 @@ export function KarpenterNodeClaimRenderer({ data, onNavigate }: KarpenterNodeCl
73
74
  value={nodeClassRef.name ? (
74
75
  <ResourceLink
75
76
  name={nodeClassRef.name}
76
- kind={(nodeClassRef.kind || 'EC2NodeClass').toLowerCase() + 's'}
77
+ kind={kindToPlural(nodeClassRef.kind || 'EC2NodeClass')}
77
78
  namespace=""
78
79
  group={nodeClassRef.group}
79
80
  label={`${nodeClassRef.kind || 'EC2NodeClass'}/${nodeClassRef.name}`}
@@ -1,5 +1,6 @@
1
1
  import { Server, Settings, Shield, Cpu, Tag } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { kindToPlural } from '../../../utils/navigation'
3
4
  import {
4
5
  getNodePoolStatus,
5
6
  getNodePoolNodeClassRef,
@@ -47,7 +48,7 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
47
48
  value={nodeClassRef?.name ? (
48
49
  <ResourceLink
49
50
  name={nodeClassRef.name}
50
- kind={(nodeClassRef.kind || 'EC2NodeClass').toLowerCase() + 's'}
51
+ kind={kindToPlural(nodeClassRef.kind || 'EC2NodeClass')}
51
52
  namespace=""
52
53
  group={nodeClassRef.group}
53
54
  label={getNodePoolNodeClassRef(data)}
@@ -1,5 +1,6 @@
1
1
  import { Cpu } from 'lucide-react'
2
2
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { kindToPlural } from '../../../utils/navigation'
3
4
  import {
4
5
  getScaledObjectStatus,
5
6
  getScaledObjectTarget,
@@ -66,7 +67,7 @@ export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectR
66
67
  return (
67
68
  <ResourceLink
68
69
  name={target.name}
69
- kind={(target.kind || 'Deployment').toLowerCase() + 's'}
70
+ kind={kindToPlural(target.kind || 'Deployment')}
70
71
  namespace={data.metadata?.namespace || ''}
71
72
  label={getScaledObjectTarget(data)}
72
73
  onNavigate={onNavigate}
@@ -1,6 +1,7 @@
1
1
  import { Radio, Filter, FileType, Inbox, ArrowRightLeft } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { kindToPlural } from '../../../utils/navigation'
4
5
  import { getKnativeConditionStatus } from '../resource-utils-knative'
5
6
 
6
7
  interface RendererProps {
@@ -48,7 +49,7 @@ export function BrokerRenderer({ data, onNavigate }: RendererProps) {
48
49
  <Property label="Dead Letter" value={
49
50
  <ResourceLink
50
51
  name={delivery.deadLetterSink.ref.name}
51
- kind={delivery.deadLetterSink.ref.kind?.toLowerCase() + 's'}
52
+ kind={kindToPlural(delivery.deadLetterSink.ref.kind || 'Service')}
52
53
  namespace={delivery.deadLetterSink.ref.namespace || ns}
53
54
  onNavigate={onNavigate}
54
55
  />
@@ -106,7 +107,7 @@ export function TriggerRenderer({ data, onNavigate }: RendererProps) {
106
107
  subscriberRef ? (
107
108
  <ResourceLink
108
109
  name={subscriberRef.name}
109
- kind={(subscriberRef.kind?.toLowerCase() || 'services') + (subscriberRef.kind?.toLowerCase().endsWith('s') ? '' : 's')}
110
+ kind={kindToPlural(subscriberRef.kind || 'Service')}
110
111
  namespace={subscriberRef.namespace || ns}
111
112
  onNavigate={onNavigate}
112
113
  />
@@ -248,7 +249,7 @@ export function SubscriptionRenderer({ data, onNavigate }: RendererProps) {
248
249
  <Property label="Channel" value={
249
250
  <ResourceLink
250
251
  name={channelRef.name}
251
- kind={(channelRef.kind?.toLowerCase() || 'channels') + (channelRef.kind?.toLowerCase().endsWith('s') ? '' : 's')}
252
+ kind={kindToPlural(channelRef.kind || 'Channel')}
252
253
  namespace={ns}
253
254
  onNavigate={onNavigate}
254
255
  />
@@ -258,7 +259,7 @@ export function SubscriptionRenderer({ data, onNavigate }: RendererProps) {
258
259
  subscriberRef ? (
259
260
  <ResourceLink
260
261
  name={subscriberRef.name}
261
- kind={(subscriberRef.kind?.toLowerCase() || 'services') + (subscriberRef.kind?.toLowerCase().endsWith('s') ? '' : 's')}
262
+ kind={kindToPlural(subscriberRef.kind || 'Service')}
262
263
  namespace={subscriberRef.namespace || ns}
263
264
  onNavigate={onNavigate}
264
265
  />
@@ -270,7 +271,7 @@ export function SubscriptionRenderer({ data, onNavigate }: RendererProps) {
270
271
  replyRef ? (
271
272
  <ResourceLink
272
273
  name={replyRef.name}
273
- kind={(replyRef.kind?.toLowerCase() || 'channels') + (replyRef.kind?.toLowerCase().endsWith('s') ? '' : 's')}
274
+ kind={kindToPlural(replyRef.kind || 'Channel')}
274
275
  namespace={replyRef.namespace || ns}
275
276
  onNavigate={onNavigate}
276
277
  />
@@ -288,7 +289,7 @@ export function SubscriptionRenderer({ data, onNavigate }: RendererProps) {
288
289
  <Property label="Sink" value={
289
290
  <ResourceLink
290
291
  name={delivery.deadLetterSink.ref.name}
291
- kind={(delivery.deadLetterSink.ref.kind?.toLowerCase() || 'services') + 's'}
292
+ kind={kindToPlural(delivery.deadLetterSink.ref.kind || 'Service')}
292
293
  namespace={delivery.deadLetterSink.ref.namespace || ns}
293
294
  onNavigate={onNavigate}
294
295
  />
@@ -1,6 +1,7 @@
1
1
  import { ListOrdered, GitFork } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { kindToPlural } from '../../../utils/navigation'
4
5
  import { getKnativeConditionStatus } from '../resource-utils-knative'
5
6
 
6
7
  interface RendererProps {
@@ -15,7 +16,7 @@ function RefDisplay({ ref: destRef, ns, onNavigate }: { ref: any; ns: string; on
15
16
  return (
16
17
  <ResourceLink
17
18
  name={destRef.ref.name}
18
- kind={(destRef.ref.kind?.toLowerCase() || 'services') + (destRef.ref.kind?.toLowerCase().endsWith('s') ? '' : 's')}
19
+ kind={kindToPlural(destRef.ref.kind || 'Service')}
19
20
  namespace={destRef.ref.namespace || ns}
20
21
  onNavigate={onNavigate}
21
22
  />
@@ -1,6 +1,7 @@
1
1
  import { Network, ShieldCheck, Server, Globe } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { kindToPlural } from '../../../utils/navigation'
4
5
  import { getKnativeConditionStatus } from '../resource-utils-knative'
5
6
 
6
7
  interface RendererProps {
@@ -169,7 +170,7 @@ export function ServerlessServiceRenderer({ data, onNavigate }: RendererProps) {
169
170
  <Property label="Target" value={
170
171
  <ResourceLink
171
172
  name={objectRef.name}
172
- kind={(objectRef.kind?.toLowerCase() || 'deployments') + 's'}
173
+ kind={kindToPlural(objectRef.kind || 'Deployment')}
173
174
  namespace={ns}
174
175
  onNavigate={onNavigate}
175
176
  />
@@ -216,7 +217,7 @@ export function DomainMappingRenderer({ data, onNavigate }: RendererProps) {
216
217
  <Property label="Target" value={
217
218
  <ResourceLink
218
219
  name={ref.name}
219
- kind={(ref.kind?.toLowerCase() || 'services') + (ref.kind?.toLowerCase().endsWith('s') ? '' : 's')}
220
+ kind={kindToPlural(ref.kind || 'Service')}
220
221
  namespace={ns}
221
222
  onNavigate={onNavigate}
222
223
  />
@@ -1,6 +1,7 @@
1
1
  import { Clock, Server, Container, Link2 } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { kindToPlural } from '../../../utils/navigation'
4
5
  import { getKnativeConditionStatus } from '../resource-utils-knative'
5
6
 
6
7
  interface RendererProps {
@@ -16,7 +17,7 @@ function SinkProperty({ sink, ns, onNavigate }: { sink: any; ns: string; onNavig
16
17
  <Property label="Sink" value={
17
18
  <ResourceLink
18
19
  name={sink.ref.name}
19
- kind={(sink.ref.kind?.toLowerCase() || 'services') + (sink.ref.kind?.toLowerCase().endsWith('s') ? '' : 's')}
20
+ kind={kindToPlural(sink.ref.kind || 'Service')}
20
21
  namespace={sink.ref.namespace || ns}
21
22
  onNavigate={onNavigate}
22
23
  />
@@ -195,7 +196,7 @@ export function SinkBindingRenderer({ data, onNavigate }: RendererProps) {
195
196
  <Property label="Subject" value={
196
197
  <ResourceLink
197
198
  name={subjectRef.name}
198
- kind={(subjectRef.kind?.toLowerCase() || 'deployments') + 's'}
199
+ kind={kindToPlural(subjectRef.kind || 'Deployment')}
199
200
  namespace={subjectRef.namespace || ns}
200
201
  onNavigate={onNavigate}
201
202
  />
@@ -1,6 +1,7 @@
1
1
  import { Cpu } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { kindToPlural } from '../../../utils/navigation'
4
5
 
5
6
  interface VPARendererProps {
6
7
  data: any
@@ -65,7 +66,7 @@ export function VPARenderer({ data, onNavigate }: VPARendererProps) {
65
66
  targetRef.name ? (
66
67
  <ResourceLink
67
68
  name={targetRef.name}
68
- kind={(targetRef.kind || 'Deployment').toLowerCase() + 's'}
69
+ kind={kindToPlural(targetRef.kind || 'Deployment')}
69
70
  namespace={data.metadata?.namespace || ''}
70
71
  label={`${targetRef.kind}/${targetRef.name}`}
71
72
  onNavigate={onNavigate}
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect } from 'react'
2
2
  import { Server, ExternalLink, Scale, Minus, Plus, Loader2 } from 'lucide-react'
3
3
  import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { DialogPortal } from '../../ui/DialogPortal'
4
5
 
5
6
  interface WorkloadRendererProps {
6
7
  kind: string
@@ -213,67 +214,64 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
213
214
  </Section>
214
215
 
215
216
  {/* Scale Dialog */}
216
- {showScaleDialog && (
217
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
218
- <div className="bg-theme-surface border border-theme-border rounded-lg shadow-xl w-80 p-4">
219
- <h3 className="text-sm font-medium text-theme-text-primary mb-4">
220
- Scale {metadata.name}
221
- </h3>
217
+ <DialogPortal open={showScaleDialog} onClose={() => setShowScaleDialog(false)} className="w-80 p-4">
218
+ <h3 className="text-sm font-medium text-theme-text-primary mb-4">
219
+ Scale {metadata.name}
220
+ </h3>
222
221
 
223
- <div className="flex items-center justify-center gap-4 mb-4">
224
- <button
225
- onClick={() => setTargetReplicas(Math.max(0, targetReplicas - 1))}
226
- className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
227
- disabled={targetReplicas <= 0}
228
- >
229
- <Minus className="w-5 h-5" />
230
- </button>
222
+ <div className="flex items-center justify-center gap-4 mb-4">
223
+ <button
224
+ onClick={() => setTargetReplicas(Math.max(0, targetReplicas - 1))}
225
+ className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
226
+ disabled={targetReplicas <= 0}
227
+ >
228
+ <Minus className="w-5 h-5" />
229
+ </button>
231
230
 
232
- <input
233
- type="number"
234
- min="0"
235
- max="10000"
236
- value={targetReplicas}
237
- onChange={(e) => setTargetReplicas(Math.min(10000, Math.max(0, parseInt(e.target.value) || 0)))}
238
- className="w-20 text-center text-2xl font-semibold bg-theme-elevated border border-theme-border rounded-lg py-2 text-theme-text-primary focus:outline-none focus:border-blue-500"
239
- />
231
+ <input
232
+ type="number"
233
+ min="0"
234
+ max="10000"
235
+ value={targetReplicas}
236
+ onChange={(e) => setTargetReplicas(Math.min(10000, Math.max(0, parseInt(e.target.value) || 0)))}
237
+ className="w-20 text-center text-2xl font-semibold bg-theme-elevated border border-theme-border rounded-lg py-2 text-theme-text-primary focus:outline-none focus:border-blue-500"
238
+ autoFocus
239
+ />
240
240
 
241
- <button
242
- onClick={() => setTargetReplicas(Math.min(10000, targetReplicas + 1))}
243
- disabled={targetReplicas >= 10000}
244
- className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
245
- >
246
- <Plus className="w-5 h-5" />
247
- </button>
248
- </div>
241
+ <button
242
+ onClick={() => setTargetReplicas(Math.min(10000, targetReplicas + 1))}
243
+ disabled={targetReplicas >= 10000}
244
+ className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
245
+ >
246
+ <Plus className="w-5 h-5" />
247
+ </button>
248
+ </div>
249
249
 
250
- <div className="text-xs text-theme-text-tertiary text-center mb-4">
251
- Current: {spec.replicas || 0} replicas
252
- {targetReplicas !== (spec.replicas || 0) && (
253
- <span className="text-theme-text-secondary">
254
- {' '}→ {targetReplicas}
255
- </span>
256
- )}
257
- </div>
250
+ <div className="text-xs text-theme-text-tertiary text-center mb-4">
251
+ Current: {spec.replicas || 0} replicas
252
+ {targetReplicas !== (spec.replicas || 0) && (
253
+ <span className="text-theme-text-secondary">
254
+ {' '}→ {targetReplicas}
255
+ </span>
256
+ )}
257
+ </div>
258
258
 
259
- <div className="flex gap-2">
260
- <button
261
- onClick={() => setShowScaleDialog(false)}
262
- className="flex-1 px-3 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary bg-theme-elevated hover:bg-theme-hover border border-theme-border rounded-lg transition-colors"
263
- >
264
- Cancel
265
- </button>
266
- <button
267
- onClick={handleScale}
268
- disabled={isScalePending || targetReplicas === (spec.replicas || 0)}
269
- className="flex-1 px-3 py-2 text-sm text-white bg-blue-600 hover:bg-blue-500 disabled:bg-blue-600/50 disabled:cursor-not-allowed rounded-lg transition-colors"
270
- >
271
- {isScalePending ? 'Scaling...' : 'Apply'}
272
- </button>
273
- </div>
274
- </div>
259
+ <div className="flex gap-2">
260
+ <button
261
+ onClick={() => setShowScaleDialog(false)}
262
+ className="flex-1 px-3 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary bg-theme-elevated hover:bg-theme-hover border border-theme-border rounded-lg transition-colors"
263
+ >
264
+ Cancel
265
+ </button>
266
+ <button
267
+ onClick={handleScale}
268
+ disabled={isScalePending || targetReplicas === (spec.replicas || 0)}
269
+ className="flex-1 px-3 py-2 text-sm text-white bg-blue-600 hover:bg-blue-500 disabled:bg-blue-600/50 disabled:cursor-not-allowed rounded-lg transition-colors"
270
+ >
271
+ {isScalePending ? 'Scaling...' : 'Apply'}
272
+ </button>
275
273
  </div>
276
- )}
274
+ </DialogPortal>
277
275
 
278
276
  <Section title="Strategy">
279
277
  <PropertyList>