@skyhook-io/k8s-ui 1.3.0 → 1.3.2
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 +2 -2
- package/src/components/dock/BottomDock.tsx +23 -5
- package/src/components/dock/DockContext.tsx +9 -1
- package/src/components/logs/LogsViewer.tsx +8 -3
- package/src/components/logs/WorkloadLogsViewer.tsx +8 -4
- package/src/components/timeline/DiffViewer.tsx +131 -0
- package/src/components/timeline/TimelineList.tsx +817 -0
- package/src/components/timeline/index.ts +2 -0
- package/src/components/topology/GroupNode.tsx +189 -79
- package/src/components/topology/TopologyGraph.tsx +286 -52
- package/src/components/topology/layout.ts +288 -92
- package/src/components/topology/topology.css +111 -0
- package/src/components/ui/Toast.tsx +74 -27
- package/src/theme/variables.css +7 -2
- package/src/types/core.ts +39 -0
- package/src/utils/download.ts +18 -3
- package/tsconfig.json +0 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { memo } from 'react'
|
|
2
2
|
import { NodeProps, Handle, Position } from '@xyflow/react'
|
|
3
|
-
import {
|
|
3
|
+
import { Box, LayoutGrid, Maximize2, Minus, Tag, Workflow } from 'lucide-react'
|
|
4
|
+
import { healthToSeverity, SEVERITY_DOT } from '../../utils/badge-colors'
|
|
5
|
+
import type { HealthStatus } from '../../types'
|
|
6
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
|
+
import type { WorkloadCard, GroupDisplayLevel } from './layout'
|
|
4
8
|
|
|
5
9
|
interface GroupNodeData {
|
|
6
10
|
type: 'namespace' | 'app' | 'label'
|
|
@@ -8,8 +12,16 @@ interface GroupNodeData {
|
|
|
8
12
|
label?: string
|
|
9
13
|
nodeCount: number
|
|
10
14
|
collapsed: boolean
|
|
11
|
-
|
|
15
|
+
displayLevel: GroupDisplayLevel
|
|
16
|
+
onSetLevel: (groupId: string, level: GroupDisplayLevel) => void
|
|
17
|
+
onCardClick?: (nodeId: string) => void
|
|
18
|
+
onMaximizeNamespace?: (namespace: string) => void
|
|
12
19
|
hideHeader?: boolean
|
|
20
|
+
worstStatus?: HealthStatus
|
|
21
|
+
unhealthyCount?: number
|
|
22
|
+
kindCounts?: Record<string, number>
|
|
23
|
+
workloadCards?: WorkloadCard[]
|
|
24
|
+
gridColumns?: number
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
export const GroupNode = memo(function GroupNode({
|
|
@@ -18,7 +30,14 @@ export const GroupNode = memo(function GroupNode({
|
|
|
18
30
|
width,
|
|
19
31
|
height,
|
|
20
32
|
}: NodeProps & { data: GroupNodeData }) {
|
|
21
|
-
const {
|
|
33
|
+
const {
|
|
34
|
+
type, name, label, nodeCount, displayLevel,
|
|
35
|
+
onSetLevel, onCardClick, onMaximizeNamespace, hideHeader,
|
|
36
|
+
worstStatus, unhealthyCount, kindCounts,
|
|
37
|
+
workloadCards, gridColumns,
|
|
38
|
+
} = data
|
|
39
|
+
const hasProblems = (unhealthyCount ?? 0) > 0
|
|
40
|
+
const statusDotColor = hasProblems && worstStatus ? SEVERITY_DOT[healthToSeverity(worstStatus)] : null
|
|
22
41
|
|
|
23
42
|
const getIcon = () => {
|
|
24
43
|
switch (type) {
|
|
@@ -33,7 +52,6 @@ export const GroupNode = memo(function GroupNode({
|
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
const getBorderStyle = (): React.CSSProperties => {
|
|
36
|
-
// Must set full 'border' property to override ReactFlow's --xy-node-border
|
|
37
55
|
switch (type) {
|
|
38
56
|
case 'namespace':
|
|
39
57
|
return { border: '2px solid var(--group-border-namespace)' }
|
|
@@ -87,112 +105,204 @@ export const GroupNode = memo(function GroupNode({
|
|
|
87
105
|
|
|
88
106
|
const Icon = getIcon()
|
|
89
107
|
|
|
90
|
-
//
|
|
91
|
-
|
|
108
|
+
// Invisible handles (shared across all levels)
|
|
109
|
+
const handles = (
|
|
110
|
+
<>
|
|
111
|
+
<Handle type="target" position={Position.Left} className="!bg-transparent !border-0 !w-0 !h-0" />
|
|
112
|
+
<Handle type="source" position={Position.Right} className="!bg-transparent !border-0 !w-0 !h-0" />
|
|
113
|
+
</>
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// Level control buttons — shared across all headers
|
|
117
|
+
const TIP_DELAY = 100
|
|
118
|
+
const levelControls = (
|
|
119
|
+
<div className="flex items-center gap-0.5 shrink-0" onClick={(e) => e.stopPropagation()}>
|
|
120
|
+
<Tooltip content="Collapse" delay={TIP_DELAY} position="bottom">
|
|
121
|
+
<button
|
|
122
|
+
className={`p-1 rounded transition-colors ${displayLevel === 'chip' ? 'opacity-40' : 'hover:bg-white/10'}`}
|
|
123
|
+
onClick={() => displayLevel !== 'chip' && onSetLevel(id, 'chip')}
|
|
124
|
+
disabled={displayLevel === 'chip'}
|
|
125
|
+
>
|
|
126
|
+
<Minus className="w-4 h-4" style={getIconStyle()} />
|
|
127
|
+
</button>
|
|
128
|
+
</Tooltip>
|
|
129
|
+
<Tooltip content="Workload cards" delay={TIP_DELAY} position="bottom">
|
|
130
|
+
<button
|
|
131
|
+
className={`p-1 rounded transition-colors ${displayLevel === 'cardGrid' ? 'opacity-40' : 'hover:bg-white/10'}`}
|
|
132
|
+
onClick={() => displayLevel !== 'cardGrid' && onSetLevel(id, 'cardGrid')}
|
|
133
|
+
disabled={displayLevel === 'cardGrid'}
|
|
134
|
+
>
|
|
135
|
+
<LayoutGrid className="w-4 h-4" style={getIconStyle()} />
|
|
136
|
+
</button>
|
|
137
|
+
</Tooltip>
|
|
138
|
+
<Tooltip content="Full topology" delay={TIP_DELAY} position="bottom">
|
|
139
|
+
<button
|
|
140
|
+
className={`p-1 rounded transition-colors ${displayLevel === 'topology' ? 'opacity-40' : 'hover:bg-white/10'}`}
|
|
141
|
+
onClick={() => displayLevel !== 'topology' && onSetLevel(id, 'topology')}
|
|
142
|
+
disabled={displayLevel === 'topology'}
|
|
143
|
+
>
|
|
144
|
+
<Workflow className="w-4 h-4" style={getIconStyle()} />
|
|
145
|
+
</button>
|
|
146
|
+
</Tooltip>
|
|
147
|
+
{onMaximizeNamespace && type === 'namespace' && (
|
|
148
|
+
<Tooltip content="Focus namespace" delay={TIP_DELAY} position="bottom">
|
|
149
|
+
<button
|
|
150
|
+
className="p-1 rounded hover:bg-white/10 transition-colors"
|
|
151
|
+
onClick={() => onMaximizeNamespace(name)}
|
|
152
|
+
>
|
|
153
|
+
<Maximize2 className="w-4 h-4" style={getIconStyle()} />
|
|
154
|
+
</button>
|
|
155
|
+
</Tooltip>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// ── Level 1: Chip (compact collapsed card) ──
|
|
161
|
+
if (displayLevel === 'chip') {
|
|
162
|
+
// Size tier: 0-9 → 0, 10-99 → 1, 100-999 → 2, 1000+ → 3
|
|
163
|
+
const tier = nodeCount === 0 ? 0 : Math.min(3, Math.floor(Math.log10(nodeCount)))
|
|
164
|
+
const nameSize = ['text-sm', 'text-xl', 'text-4xl', 'text-6xl'][tier]
|
|
165
|
+
const iconSize = ['w-3.5 h-3.5', 'w-5 h-5', 'w-9 h-9', 'w-12 h-12'][tier]
|
|
166
|
+
const maxPills = [2, 5, 8, 12][tier]
|
|
167
|
+
const chipPadding = ['8px 10px', '10px 12px', '14px 16px', '16px 20px'][tier]
|
|
168
|
+
|
|
169
|
+
const kindPills = kindCounts
|
|
170
|
+
? Object.entries(kindCounts)
|
|
171
|
+
.sort(([, a], [, b]) => b - a)
|
|
172
|
+
.slice(0, maxPills)
|
|
173
|
+
: []
|
|
174
|
+
|
|
92
175
|
return (
|
|
93
176
|
<>
|
|
94
|
-
|
|
95
|
-
type="target"
|
|
96
|
-
position={Position.Left}
|
|
97
|
-
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
98
|
-
/>
|
|
99
|
-
|
|
177
|
+
{handles}
|
|
100
178
|
<div
|
|
101
|
-
className="rounded-xl
|
|
102
|
-
|
|
103
|
-
style={{ ...getBorderStyle(), ...getHeaderBgStyle() }}
|
|
179
|
+
className="rounded-xl group-header-scaled overflow-hidden"
|
|
180
|
+
style={{ ...getBorderStyle(), ...getHeaderBgStyle(), padding: chipPadding, width: width || '100%', height: height || '100%' }}
|
|
104
181
|
>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<Icon className=
|
|
108
|
-
<span className=
|
|
109
|
-
|
|
110
|
-
|
|
182
|
+
{/* Header row: icon + name + count + status + controls */}
|
|
183
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
184
|
+
<Icon className={`${iconSize} shrink-0`} style={getIconStyle()} />
|
|
185
|
+
<span className={`${nameSize} font-bold truncate`} style={getLabelStyle()}>{name}</span>
|
|
186
|
+
<span className="text-[10px] text-theme-text-secondary shrink-0 bg-theme-surface/50 rounded px-1.5 py-0.5">{nodeCount}</span>
|
|
187
|
+
{statusDotColor && (
|
|
188
|
+
<span className={`w-2 h-2 rounded-full shrink-0 ${statusDotColor}`} />
|
|
111
189
|
)}
|
|
190
|
+
<div className="ml-auto shrink-0">{levelControls}</div>
|
|
112
191
|
</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
192
|
+
{/* Kind pills row: colored icons with counts */}
|
|
193
|
+
{kindPills.length > 0 && (
|
|
194
|
+
<div className="flex flex-wrap gap-1 mt-1.5">
|
|
195
|
+
{kindPills.map(([kind, count]) => (
|
|
196
|
+
<div key={kind} className="flex items-center gap-1 bg-theme-surface/50 rounded px-1.5 py-0.5">
|
|
197
|
+
<span className={`topology-icon topology-icon-${kind.toLowerCase()}`} style={{ width: 10, height: 10, fontSize: 6, borderRadius: 2 }} />
|
|
198
|
+
<span className="text-[10px] text-theme-text-secondary">{count} {kind}{count > 1 ? 's' : ''}</span>
|
|
199
|
+
</div>
|
|
200
|
+
))}
|
|
201
|
+
{kindCounts && Object.keys(kindCounts).length > maxPills && (
|
|
202
|
+
<span className="text-[10px] text-theme-text-tertiary self-center">+{Object.keys(kindCounts).length - maxPills}</span>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
116
206
|
</div>
|
|
207
|
+
</>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Level 2: Card Grid (workload cards inside namespace) ──
|
|
212
|
+
if (displayLevel === 'cardGrid' && workloadCards) {
|
|
213
|
+
const cols = gridColumns || Math.min(4, workloadCards.length || 1)
|
|
214
|
+
return (
|
|
215
|
+
<>
|
|
216
|
+
{handles}
|
|
217
|
+
<div
|
|
218
|
+
className="rounded-xl overflow-hidden group-header-scaled"
|
|
219
|
+
style={{ ...getBorderStyle(), width: width || '100%', height: height || '100%' }}
|
|
220
|
+
>
|
|
221
|
+
{/* Header bar with level controls */}
|
|
222
|
+
<div
|
|
223
|
+
className="w-full relative flex items-center"
|
|
224
|
+
style={{ ...getHeaderBgStyle(), padding: '8px 12px' }}
|
|
225
|
+
>
|
|
226
|
+
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
227
|
+
<Icon className="shrink-0 w-5 h-5" style={getIconStyle()} />
|
|
228
|
+
<span className="font-bold truncate text-base" style={getLabelStyle()}>{name}</span>
|
|
229
|
+
<span className="shrink-0 text-xs text-theme-text-secondary bg-theme-surface/50 rounded px-1.5 py-0.5">
|
|
230
|
+
{workloadCards.length}
|
|
231
|
+
</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="shrink-0 ml-2">{levelControls}</div>
|
|
234
|
+
</div>
|
|
117
235
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
236
|
+
{/* Workload card grid */}
|
|
237
|
+
<div
|
|
238
|
+
style={{
|
|
239
|
+
display: 'grid',
|
|
240
|
+
gridTemplateColumns: `repeat(${cols}, 200px)`,
|
|
241
|
+
gap: '6px',
|
|
242
|
+
padding: '8px 14px 14px',
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{workloadCards.map(card => {
|
|
246
|
+
const statusClass = card.status === 'unhealthy' ? 'grid-card-unhealthy'
|
|
247
|
+
: card.status === 'degraded' ? 'grid-card-degraded'
|
|
248
|
+
: card.status === 'unknown' ? 'grid-card-unknown' : ''
|
|
249
|
+
return (
|
|
250
|
+
<div
|
|
251
|
+
key={card.id}
|
|
252
|
+
className={`grid-card ${statusClass}`}
|
|
253
|
+
onClick={(e) => { e.stopPropagation(); onCardClick?.(card.id) }}
|
|
254
|
+
>
|
|
255
|
+
<div className="grid-card-header">
|
|
256
|
+
<span className={`topology-icon topology-icon-${card.kind.toLowerCase()}`} />
|
|
257
|
+
<span className="grid-card-kind">{card.kind}</span>
|
|
258
|
+
<span className="grid-card-count">{card.resourceCount}</span>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="grid-card-name">{card.name}</div>
|
|
261
|
+
{card.subtitle && (
|
|
262
|
+
<div className="grid-card-subtitle">{card.subtitle}</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
})}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
123
269
|
</>
|
|
124
270
|
)
|
|
125
271
|
}
|
|
126
272
|
|
|
127
|
-
//
|
|
128
|
-
// Children are rendered automatically by ReactFlow via parentId
|
|
273
|
+
// ── Level 3: Topology (full expanded container) ──
|
|
129
274
|
return (
|
|
130
275
|
<>
|
|
131
|
-
|
|
132
|
-
type="target"
|
|
133
|
-
position={Position.Left}
|
|
134
|
-
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
135
|
-
/>
|
|
136
|
-
|
|
137
|
-
{/* Container with border - use explicit dimensions from props */}
|
|
138
|
-
{/* Top position adjusts based on CSS variable set by ViewportController */}
|
|
276
|
+
{handles}
|
|
139
277
|
<div
|
|
140
|
-
className=
|
|
278
|
+
className={`absolute left-0 box-border isolate overflow-hidden group-container-adjusted ${hideHeader ? '' : 'rounded-xl bg-theme-surface/40'}`}
|
|
141
279
|
style={{
|
|
142
280
|
width: width || '100%',
|
|
143
281
|
height: height || '100%',
|
|
144
|
-
...getBorderStyle()
|
|
282
|
+
...(hideHeader ? {} : getBorderStyle())
|
|
145
283
|
}}
|
|
146
284
|
>
|
|
147
|
-
{/* Header bar - background extends full width, content scales, count fixed right */}
|
|
148
|
-
{/* Hidden when hideHeader is true (single namespace view) */}
|
|
149
285
|
{!hideHeader && (
|
|
150
286
|
<div
|
|
151
|
-
className="w-full
|
|
152
|
-
|
|
153
|
-
style={getHeaderBgStyle()}
|
|
287
|
+
className="w-full relative flex items-center"
|
|
288
|
+
style={{ ...getHeaderBgStyle(), padding: '12px 16px' }}
|
|
154
289
|
>
|
|
155
|
-
{/* Scaled content */}
|
|
156
290
|
<div
|
|
157
|
-
className="flex items-center group-header-scaled"
|
|
158
|
-
style={{
|
|
159
|
-
padding: '20px 24px',
|
|
160
|
-
gap: '16px',
|
|
161
|
-
}}
|
|
291
|
+
className="flex items-center gap-3 min-w-0 flex-1 group-header-scaled"
|
|
162
292
|
>
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
style={getIconStyle()}
|
|
166
|
-
/>
|
|
167
|
-
<Icon
|
|
168
|
-
className="shrink-0 w-9 h-9"
|
|
169
|
-
style={getIconStyle()}
|
|
170
|
-
/>
|
|
171
|
-
<span
|
|
172
|
-
className="font-bold truncate text-4xl"
|
|
173
|
-
style={getLabelStyle()}
|
|
174
|
-
>
|
|
175
|
-
{name}
|
|
176
|
-
</span>
|
|
293
|
+
<Icon className="shrink-0 w-7 h-7" style={getIconStyle()} />
|
|
294
|
+
<span className="font-bold truncate text-2xl" style={getLabelStyle()}>{name}</span>
|
|
177
295
|
{label && (
|
|
178
|
-
<span className="text-sm text-theme-text-secondary truncate">
|
|
179
|
-
({label})
|
|
180
|
-
</span>
|
|
296
|
+
<span className="text-sm text-theme-text-secondary truncate">({label})</span>
|
|
181
297
|
)}
|
|
298
|
+
<span className="shrink-0 text-xs text-theme-text-secondary bg-theme-surface/50 rounded px-1.5 py-0.5">
|
|
299
|
+
{nodeCount}
|
|
300
|
+
</span>
|
|
182
301
|
</div>
|
|
183
|
-
|
|
184
|
-
<span className="absolute right-4 top-4 shrink-0 font-medium text-sm text-theme-text-secondary bg-theme-surface/70 rounded-lg px-3 py-1">
|
|
185
|
-
{nodeCount}
|
|
186
|
-
</span>
|
|
302
|
+
<div className="shrink-0 ml-2">{levelControls}</div>
|
|
187
303
|
</div>
|
|
188
304
|
)}
|
|
189
305
|
</div>
|
|
190
|
-
|
|
191
|
-
<Handle
|
|
192
|
-
type="source"
|
|
193
|
-
position={Position.Right}
|
|
194
|
-
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
195
|
-
/>
|
|
196
306
|
</>
|
|
197
307
|
)
|
|
198
308
|
})
|