@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.
- package/package.json +4 -1
- package/src/components/resources/ResourcesView.tsx +50 -43
- package/src/components/resources/renderers/HPARenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +2 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +7 -6
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +3 -2
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/VPARenderer.tsx +2 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +53 -55
- package/src/components/shared/ResourceActionsBar.tsx +141 -142
- package/src/components/shared/ResourceRendererDispatch.tsx +40 -4
- package/src/components/shared/index.ts +1 -1
- package/src/components/topology/TopologyGraph.tsx +282 -19
- package/src/components/ui/ConfirmDialog.tsx +85 -130
- package/src/components/ui/DialogPortal.tsx +82 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +21 -1
- package/src/types/core.ts +1 -2
- package/src/utils/navigation.test.ts +142 -0
- package/src/utils/navigation.ts +52 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/skyhook-io/radar",
|
|
@@ -23,10 +23,12 @@
|
|
|
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": {
|
|
29
30
|
"@monaco-editor/react": "^4.7.0",
|
|
31
|
+
"html-to-image": "^1.11.0",
|
|
30
32
|
"react-virtuoso": "^4.18.1",
|
|
31
33
|
"shiki": "^4.0.0"
|
|
32
34
|
},
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
"react": "^19.2.4",
|
|
59
61
|
"react-dom": "^19.2.4",
|
|
60
62
|
"typescript": "^5.9.3",
|
|
63
|
+
"vitest": "^4.0.18",
|
|
61
64
|
"yaml": "^2.7.1"
|
|
62
65
|
}
|
|
63
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="
|
|
3046
|
-
<
|
|
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
|
|
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=
|
|
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-
|
|
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=
|
|
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-
|
|
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
|
-
|
|
3799
|
-
|
|
3800
|
-
<
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
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')
|
|
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')
|
|
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')
|
|
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')
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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')
|
|
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
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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>
|