@skyhook-io/k8s-ui 1.7.7 → 1.7.9
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 +6 -1
- package/src/components/charts/PrometheusChartsView.tsx +233 -0
- package/src/components/charts/index.ts +13 -0
- package/src/components/checks/ChecksView.tsx +5 -1
- package/src/components/gitops/GitOpsTableView.tsx +1 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +3 -3
- package/src/components/issues/IssuesView.tsx +374 -0
- package/src/components/issues/index.ts +24 -0
- package/src/components/issues/issues.test.ts +100 -0
- package/src/components/issues/severity.ts +125 -0
- package/src/components/issues/types.ts +194 -0
- package/src/components/resources/ResourcesView.tsx +2 -19
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +1 -1
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +1 -1
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +1 -1
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -1
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/CrossplanePackageRenderer.tsx +5 -5
- package/src/components/resources/renderers/CrossplaneProviderConfigRenderer.tsx +1 -1
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -1
- package/src/components/resources/renderers/PodRenderer.test.tsx +53 -0
- package/src/components/resources/renderers/PodRenderer.tsx +7 -1
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -1
- package/src/components/resources/renderers/WorkloadRenderer.test.tsx +41 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +49 -7
- package/src/components/resources/renderers/XRDRenderer.tsx +2 -2
- package/src/components/shared/DetailShell.tsx +107 -0
- package/src/components/shared/ManagedByChip.tsx +149 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -1
- package/src/components/shared/index.ts +2 -1
- package/src/components/timeline/TimelineSwimlanes.tsx +1320 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/topology/K8sResourceNode.tsx +60 -60
- package/src/components/topology/TopologyFilterSidebar.tsx +25 -3
- package/src/components/topology/TopologyGraph.tsx +168 -52
- package/src/components/topology/TopologySearch.tsx +17 -8
- package/src/components/topology/topology-search-match.test.ts +1 -1
- package/src/components/topology/topology.css +18 -0
- package/src/components/ui/Tooltip.tsx +37 -2
- package/src/components/workload/WorkloadView.tsx +118 -112
- package/src/index.ts +5 -0
- package/src/theme/components.css +35 -0
- package/src/types/core.ts +3 -2
- package/src/utils/env-from.ts +3 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/replica-scalers.ts +9 -0
- package/src/components/resources/resources-search-sidebar-hint.test.ts +0 -85
|
@@ -54,6 +54,7 @@ export function Tooltip({
|
|
|
54
54
|
const triggerRef = useRef<HTMLSpanElement>(null)
|
|
55
55
|
const tooltipRef = useRef<HTMLSpanElement>(null)
|
|
56
56
|
const timeoutRef = useRef<number | null>(null)
|
|
57
|
+
const hideTimeoutRef = useRef<number | null>(null)
|
|
57
58
|
const rafRef = useRef<number | null>(null)
|
|
58
59
|
|
|
59
60
|
const updatePosition = useCallback(() => {
|
|
@@ -94,12 +95,24 @@ export function Tooltip({
|
|
|
94
95
|
clearTimeout(timeoutRef.current)
|
|
95
96
|
timeoutRef.current = null
|
|
96
97
|
}
|
|
98
|
+
if (hideTimeoutRef.current) {
|
|
99
|
+
clearTimeout(hideTimeoutRef.current)
|
|
100
|
+
hideTimeoutRef.current = null
|
|
101
|
+
}
|
|
97
102
|
setIsVisible(false)
|
|
98
103
|
setCoords(null)
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
const cancelHide = () => {
|
|
107
|
+
if (hideTimeoutRef.current) {
|
|
108
|
+
clearTimeout(hideTimeoutRef.current)
|
|
109
|
+
hideTimeoutRef.current = null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
101
113
|
const showTooltip = () => {
|
|
102
114
|
if (disabled || !content) return
|
|
115
|
+
cancelHide()
|
|
103
116
|
timeoutRef.current = window.setTimeout(() => {
|
|
104
117
|
// Singleton: hide whoever was visible before us, register self
|
|
105
118
|
// as the new active tooltip. Guards against stuck duplicates.
|
|
@@ -116,6 +129,10 @@ export function Tooltip({
|
|
|
116
129
|
clearTimeout(timeoutRef.current)
|
|
117
130
|
timeoutRef.current = null
|
|
118
131
|
}
|
|
132
|
+
if (hideTimeoutRef.current) {
|
|
133
|
+
clearTimeout(hideTimeoutRef.current)
|
|
134
|
+
hideTimeoutRef.current = null
|
|
135
|
+
}
|
|
119
136
|
if (activeHide === hideRef.current) {
|
|
120
137
|
activeHide = null
|
|
121
138
|
}
|
|
@@ -123,6 +140,15 @@ export function Tooltip({
|
|
|
123
140
|
setCoords(null)
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
const scheduleHideTooltip = () => {
|
|
144
|
+
if (timeoutRef.current) {
|
|
145
|
+
clearTimeout(timeoutRef.current)
|
|
146
|
+
timeoutRef.current = null
|
|
147
|
+
}
|
|
148
|
+
if (hideTimeoutRef.current) return
|
|
149
|
+
hideTimeoutRef.current = window.setTimeout(hideTooltip, 80)
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
useEffect(() => {
|
|
127
153
|
if (isVisible) {
|
|
128
154
|
// Second rAF re-centers using the measured tooltip size; without
|
|
@@ -144,6 +170,9 @@ export function Tooltip({
|
|
|
144
170
|
if (timeoutRef.current) {
|
|
145
171
|
clearTimeout(timeoutRef.current)
|
|
146
172
|
}
|
|
173
|
+
if (hideTimeoutRef.current) {
|
|
174
|
+
clearTimeout(hideTimeoutRef.current)
|
|
175
|
+
}
|
|
147
176
|
if (rafRef.current !== null) {
|
|
148
177
|
cancelAnimationFrame(rafRef.current)
|
|
149
178
|
}
|
|
@@ -169,6 +198,10 @@ export function Tooltip({
|
|
|
169
198
|
clearTimeout(timeoutRef.current)
|
|
170
199
|
timeoutRef.current = null
|
|
171
200
|
}
|
|
201
|
+
if (hideTimeoutRef.current) {
|
|
202
|
+
clearTimeout(hideTimeoutRef.current)
|
|
203
|
+
hideTimeoutRef.current = null
|
|
204
|
+
}
|
|
172
205
|
setIsVisible(false)
|
|
173
206
|
setCoords(null)
|
|
174
207
|
}
|
|
@@ -202,7 +235,7 @@ export function Tooltip({
|
|
|
202
235
|
className={clsx('inline-flex max-w-full', wrapperClassName)}
|
|
203
236
|
style={wrapperStyle}
|
|
204
237
|
onMouseEnter={showTooltip}
|
|
205
|
-
onMouseLeave={
|
|
238
|
+
onMouseLeave={scheduleHideTooltip}
|
|
206
239
|
onFocus={showTooltip}
|
|
207
240
|
onBlur={hideTooltip}
|
|
208
241
|
// pointerdown fires before click, so the tooltip is gone before
|
|
@@ -227,7 +260,7 @@ export function Tooltip({
|
|
|
227
260
|
// max-w-xs (320px) + whitespace-normal, short tooltips
|
|
228
261
|
// still fit on one line (content shorter than max-width)
|
|
229
262
|
// and long ones wrap naturally near the trigger.
|
|
230
|
-
'max-w-xs whitespace-normal break-words
|
|
263
|
+
'max-w-xs whitespace-normal break-words',
|
|
231
264
|
className
|
|
232
265
|
)}
|
|
233
266
|
style={{
|
|
@@ -238,6 +271,8 @@ export function Tooltip({
|
|
|
238
271
|
}}
|
|
239
272
|
role="tooltip"
|
|
240
273
|
aria-hidden={coords ? undefined : true}
|
|
274
|
+
onMouseEnter={cancelHide}
|
|
275
|
+
onMouseLeave={scheduleHideTooltip}
|
|
241
276
|
>
|
|
242
277
|
{content}
|
|
243
278
|
</span>,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
BarChart3,
|
|
22
22
|
} from 'lucide-react'
|
|
23
23
|
import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom } from '../../types'
|
|
24
|
+
import type { GitOpsStatus } from '../../types/gitops'
|
|
24
25
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
25
26
|
import { refToSelectedResource, pluralToKind } from '../../utils/navigation'
|
|
26
27
|
import { gitOpsOwnerFromRelationships, type GitOpsOwnerRef } from '../../utils/gitops-owner'
|
|
@@ -44,7 +45,8 @@ import {
|
|
|
44
45
|
import { ResourceActionsBar } from '../shared/ResourceActionsBar'
|
|
45
46
|
import { EditableYamlView, SaveSuccessAnimation } from '../shared/EditableYamlView'
|
|
46
47
|
import { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } from '../shared/ResourceRendererDispatch'
|
|
47
|
-
import {
|
|
48
|
+
import { DetailShell, type DetailShellTab } from '../shared/DetailShell'
|
|
49
|
+
import { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from '../shared/ManagedByChip'
|
|
48
50
|
import { getKindColorOutline, formatKindName } from '../ui/drawer-components'
|
|
49
51
|
|
|
50
52
|
type TabType = 'overview' | 'timeline' | 'logs' | 'metrics' | 'yaml'
|
|
@@ -71,6 +73,20 @@ interface WorkloadViewProps {
|
|
|
71
73
|
/** API group for CRD resources */
|
|
72
74
|
group?: string
|
|
73
75
|
|
|
76
|
+
// ── Hosted chrome (expanded mode) ────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* A breadcrumb rendered above the identity header — e.g. when a larger
|
|
79
|
+
* surface (Radar Cloud's app page) hosts this view inside its own navigation.
|
|
80
|
+
* When set, the standalone back button is not rendered; `onBack` still backs
|
|
81
|
+
* the Escape shortcut.
|
|
82
|
+
*/
|
|
83
|
+
breadcrumb?: ReactNode
|
|
84
|
+
/**
|
|
85
|
+
* Controls injected into the shell's tab-row scope slot — e.g. a cluster /
|
|
86
|
+
* workload picker in Radar Cloud. Absent in standalone Radar.
|
|
87
|
+
*/
|
|
88
|
+
scopeControls?: ReactNode
|
|
89
|
+
|
|
74
90
|
// ── Data (injected by wrapper) ──────────────────────────────────────────
|
|
75
91
|
/** The resource data object */
|
|
76
92
|
resource?: any
|
|
@@ -126,6 +142,22 @@ interface WorkloadViewProps {
|
|
|
126
142
|
* visible (useful for hosts that haven't routed the GitOps tab yet).
|
|
127
143
|
*/
|
|
128
144
|
onOpenGitOpsResource?: (ref: GitOpsOwnerRef) => void
|
|
145
|
+
/** Owner ref resolved by the host when relationships lack enough detail, e.g. Argo labels without namespace. */
|
|
146
|
+
resolvedGitOpsOwner?: GitOpsOwnerRef | null
|
|
147
|
+
/** True when the owner exists locally and can be opened as a GitOps detail page. */
|
|
148
|
+
gitOpsOwnerVerified?: boolean
|
|
149
|
+
/** True while the host is still resolving whether the owner exists locally. */
|
|
150
|
+
gitOpsOwnerPending?: boolean
|
|
151
|
+
/** Metadata key/value that caused GitOps ownership inference, when known. */
|
|
152
|
+
gitOpsOwnerSource?: string | null
|
|
153
|
+
/** Sync/health status for the GitOps owner, when the host can resolve it. */
|
|
154
|
+
gitOpsOwnerStatus?: GitOpsStatus | null
|
|
155
|
+
/** Native Helm release that manages this resource, when detected. */
|
|
156
|
+
helmOwner?: HelmOwnerRef | null
|
|
157
|
+
/** Metadata key/value that caused native Helm ownership inference, when known. */
|
|
158
|
+
helmOwnerSource?: string | null
|
|
159
|
+
/** Open the native Helm release drawer. */
|
|
160
|
+
onOpenHelmRelease?: (ref: HelmOwnerRef) => void
|
|
129
161
|
/**
|
|
130
162
|
* Open the GitOps detail page for the resource itself, when the resource
|
|
131
163
|
* is a portal-classified GitOps CR (Argo Application/ApplicationSet/
|
|
@@ -185,6 +217,8 @@ export function WorkloadView({
|
|
|
185
217
|
onExpand,
|
|
186
218
|
initialTab,
|
|
187
219
|
group,
|
|
220
|
+
breadcrumb,
|
|
221
|
+
scopeControls,
|
|
188
222
|
// Data
|
|
189
223
|
resource,
|
|
190
224
|
relationships,
|
|
@@ -226,6 +260,14 @@ export function WorkloadView({
|
|
|
226
260
|
resolvedEnvFrom,
|
|
227
261
|
// GitOps
|
|
228
262
|
onOpenGitOpsResource,
|
|
263
|
+
resolvedGitOpsOwner,
|
|
264
|
+
gitOpsOwnerVerified = true,
|
|
265
|
+
gitOpsOwnerPending = false,
|
|
266
|
+
gitOpsOwnerSource,
|
|
267
|
+
gitOpsOwnerStatus,
|
|
268
|
+
helmOwner,
|
|
269
|
+
helmOwnerSource,
|
|
270
|
+
onOpenHelmRelease,
|
|
229
271
|
onNavigateGitOpsPath,
|
|
230
272
|
}: WorkloadViewProps) {
|
|
231
273
|
// Normalize kind: URL has plural lowercase, internal logic uses singular PascalCase
|
|
@@ -315,7 +357,8 @@ export function WorkloadView({
|
|
|
315
357
|
|
|
316
358
|
// Metadata
|
|
317
359
|
const metadata = useMemo(() => extractMetadata(kind, resource), [kind, resource])
|
|
318
|
-
const
|
|
360
|
+
const relationshipGitOpsOwner = useMemo(() => gitOpsOwnerFromRelationships(relationships), [relationships])
|
|
361
|
+
const gitopsOwner = resolvedGitOpsOwner ?? relationshipGitOpsOwner
|
|
319
362
|
// When the resource itself is a portal GitOps CR (Application, Kustomization,
|
|
320
363
|
// HelmRelease, etc.), surface a link to its dedicated GitOps detail page —
|
|
321
364
|
// the drawer's renderer is thorough but the tab has the tree + insights +
|
|
@@ -401,6 +444,18 @@ export function WorkloadView({
|
|
|
401
444
|
const status = getResourceStatus(apiKind, resource)
|
|
402
445
|
|
|
403
446
|
const showMetricsTab = isMetricsAvailable ? isMetricsAvailable(kind, resource) : false
|
|
447
|
+
const tabs: DetailShellTab<TabType>[] = [
|
|
448
|
+
{ id: 'overview', label: 'Overview', icon: <Layers className="w-4 h-4" /> },
|
|
449
|
+
{
|
|
450
|
+
id: 'timeline',
|
|
451
|
+
label: 'Timeline',
|
|
452
|
+
icon: <Activity className="w-4 h-4" />,
|
|
453
|
+
badge: resourceEvents.length > 0 ? <span className="ml-1 badge-sm bg-theme-elevated">{resourceEvents.length}</span> : undefined,
|
|
454
|
+
},
|
|
455
|
+
{ id: 'logs', label: 'Logs', icon: <Terminal className="w-4 h-4" />, hidden: !(allPods.length > 0 && renderLogsTab) },
|
|
456
|
+
{ id: 'metrics', label: 'Metrics', icon: <BarChart3 className="w-4 h-4" />, hidden: !(showMetricsTab && renderMetricsTab) },
|
|
457
|
+
{ id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
|
|
458
|
+
]
|
|
404
459
|
|
|
405
460
|
// ── Collapsed (drawer) mode ──────────────────────────────────────────────
|
|
406
461
|
if (!expanded) {
|
|
@@ -465,9 +520,10 @@ export function WorkloadView({
|
|
|
465
520
|
</button>
|
|
466
521
|
</div>
|
|
467
522
|
<p className="text-sm text-theme-text-tertiary">{namespace}</p>
|
|
468
|
-
{(gitopsOwner || (gitOpsResourcePath && onNavigateGitOpsPath)) && (
|
|
523
|
+
{(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath)) && (
|
|
469
524
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
470
|
-
{gitopsOwner && <ManagedByChip owner={gitopsOwner} onOpen={onOpenGitOpsResource} />}
|
|
525
|
+
{gitopsOwner && <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} />}
|
|
526
|
+
{helmOwner && <HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} />}
|
|
471
527
|
{gitOpsResourcePath && onNavigateGitOpsPath && (
|
|
472
528
|
<OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
|
|
473
529
|
)}
|
|
@@ -534,11 +590,10 @@ export function WorkloadView({
|
|
|
534
590
|
|
|
535
591
|
// ── Expanded (full) mode ─────────────────────────────────────────────────
|
|
536
592
|
return (
|
|
537
|
-
<
|
|
538
|
-
{
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
{/* Back button */}
|
|
593
|
+
<DetailShell
|
|
594
|
+
breadcrumb={breadcrumb}
|
|
595
|
+
nav={
|
|
596
|
+
breadcrumb ? undefined : (
|
|
542
597
|
<button
|
|
543
598
|
onClick={onBack}
|
|
544
599
|
className="p-1.5 mt-0.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors"
|
|
@@ -546,47 +601,52 @@ export function WorkloadView({
|
|
|
546
601
|
>
|
|
547
602
|
<ArrowLeft className="w-5 h-5" />
|
|
548
603
|
</button>
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
</
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
identity={
|
|
607
|
+
<>
|
|
608
|
+
<div className="flex items-center gap-3 mb-1">
|
|
609
|
+
<h1 className="text-lg font-semibold text-theme-text-primary truncate">{name}</h1>
|
|
610
|
+
<button
|
|
611
|
+
onClick={() => copyToClipboard(name, 'name')}
|
|
612
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
|
|
613
|
+
title="Copy name"
|
|
614
|
+
>
|
|
615
|
+
{copied === 'name' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
|
|
616
|
+
</button>
|
|
617
|
+
</div>
|
|
618
|
+
<div className="flex items-center gap-3 text-sm text-theme-text-secondary">
|
|
619
|
+
<span className={clsx('badge', getKindColorOutline(apiKind))}>
|
|
620
|
+
{formatKindName(apiKind)}
|
|
621
|
+
</span>
|
|
622
|
+
{status && (
|
|
623
|
+
<span className={clsx('badge', status.color)}>
|
|
624
|
+
{status.text}
|
|
565
625
|
</span>
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
{
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
{
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
{
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
{
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
{relationships
|
|
584
|
-
|
|
585
|
-
)}
|
|
586
|
-
</div>
|
|
626
|
+
)}
|
|
627
|
+
{namespace && namespace !== '_' && (
|
|
628
|
+
<span>Namespace: <span className="text-theme-text-primary">{namespace}</span></span>
|
|
629
|
+
)}
|
|
630
|
+
{metadata.find(m => m.label === 'Image') && (
|
|
631
|
+
<span className="truncate max-w-md font-mono text-xs">{metadata.find(m => m.label === 'Image')?.value}</span>
|
|
632
|
+
)}
|
|
633
|
+
{gitopsOwner && (
|
|
634
|
+
<ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} variant="block" />
|
|
635
|
+
)}
|
|
636
|
+
{helmOwner && (
|
|
637
|
+
<HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} variant="block" />
|
|
638
|
+
)}
|
|
639
|
+
{gitOpsResourcePath && onNavigateGitOpsPath && (
|
|
640
|
+
<OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
|
|
641
|
+
)}
|
|
642
|
+
{relationships?.owner && (
|
|
643
|
+
<span>Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
|
|
644
|
+
)}
|
|
587
645
|
</div>
|
|
588
|
-
|
|
589
|
-
|
|
646
|
+
</>
|
|
647
|
+
}
|
|
648
|
+
headerActions={
|
|
649
|
+
<>
|
|
590
650
|
<button
|
|
591
651
|
onClick={() => refetch()}
|
|
592
652
|
disabled={isRefreshAnimating}
|
|
@@ -601,8 +661,6 @@ export function WorkloadView({
|
|
|
601
661
|
: <RefreshCw className={clsx('w-5 h-5', refreshPhase === 'spinning' && 'animate-spin')} />
|
|
602
662
|
}
|
|
603
663
|
</button>
|
|
604
|
-
|
|
605
|
-
{/* Collapse back to drawer */}
|
|
606
664
|
{onCollapseToDrawer && (
|
|
607
665
|
<button
|
|
608
666
|
onClick={onCollapseToDrawer}
|
|
@@ -612,50 +670,15 @@ export function WorkloadView({
|
|
|
612
670
|
<Minimize2 className="w-5 h-5" />
|
|
613
671
|
</button>
|
|
614
672
|
)}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
<TabButton active={activeTab === 'timeline'} onClick={() => handleSetTab('timeline')}>
|
|
625
|
-
<Activity className="w-4 h-4" />
|
|
626
|
-
Timeline
|
|
627
|
-
{resourceEvents.length > 0 && (
|
|
628
|
-
<span className="ml-1 badge-sm bg-theme-elevated">{resourceEvents.length}</span>
|
|
629
|
-
)}
|
|
630
|
-
</TabButton>
|
|
631
|
-
{allPods.length > 0 && renderLogsTab && (
|
|
632
|
-
<TabButton active={activeTab === 'logs'} onClick={() => handleSetTab('logs')}>
|
|
633
|
-
<Terminal className="w-4 h-4" />
|
|
634
|
-
Logs
|
|
635
|
-
</TabButton>
|
|
636
|
-
)}
|
|
637
|
-
{showMetricsTab && renderMetricsTab && (
|
|
638
|
-
<TabButton active={activeTab === 'metrics'} onClick={() => handleSetTab('metrics')}>
|
|
639
|
-
<BarChart3 className="w-4 h-4" />
|
|
640
|
-
Metrics
|
|
641
|
-
</TabButton>
|
|
642
|
-
)}
|
|
643
|
-
<TabButton active={activeTab === 'yaml'} onClick={() => handleSetTab('yaml')}>
|
|
644
|
-
<FileText className="w-4 h-4" />
|
|
645
|
-
YAML
|
|
646
|
-
</TabButton>
|
|
647
|
-
</div>
|
|
648
|
-
<div className="ml-auto">
|
|
649
|
-
<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...actionsBarProps} />
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
</div>
|
|
653
|
-
|
|
654
|
-
{/* Success animation overlay */}
|
|
655
|
-
{saveSuccess && <SaveSuccessAnimation />}
|
|
656
|
-
|
|
657
|
-
{/* Tab Content */}
|
|
658
|
-
<div className="flex-1 overflow-hidden relative">
|
|
673
|
+
</>
|
|
674
|
+
}
|
|
675
|
+
tabs={tabs}
|
|
676
|
+
activeTab={activeTab}
|
|
677
|
+
onTabChange={handleSetTab}
|
|
678
|
+
scopeControls={scopeControls}
|
|
679
|
+
tabStripEnd={<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...actionsBarProps} />}
|
|
680
|
+
overlay={saveSuccess ? <SaveSuccessAnimation /> : null}
|
|
681
|
+
>
|
|
659
682
|
{activeTab === 'overview' && (
|
|
660
683
|
<InfoTab
|
|
661
684
|
resource={resource}
|
|
@@ -732,8 +755,7 @@ export function WorkloadView({
|
|
|
732
755
|
)}
|
|
733
756
|
</div>
|
|
734
757
|
)}
|
|
735
|
-
|
|
736
|
-
</div>
|
|
758
|
+
</DetailShell>
|
|
737
759
|
)
|
|
738
760
|
}
|
|
739
761
|
|
|
@@ -792,22 +814,6 @@ function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
|
|
|
792
814
|
)
|
|
793
815
|
}
|
|
794
816
|
|
|
795
|
-
function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
|
|
796
|
-
return (
|
|
797
|
-
<button
|
|
798
|
-
onClick={onClick}
|
|
799
|
-
className={clsx(
|
|
800
|
-
'flex items-center gap-1.5 px-3 py-2 text-sm font-medium border-b-2 transition-colors',
|
|
801
|
-
active
|
|
802
|
-
? 'text-theme-text-primary border-skyhook-500'
|
|
803
|
-
: 'text-theme-text-secondary border-transparent hover:text-theme-text-primary hover:border-theme-border-light'
|
|
804
|
-
)}
|
|
805
|
-
>
|
|
806
|
-
{children}
|
|
807
|
-
</button>
|
|
808
|
-
)
|
|
809
|
-
}
|
|
810
|
-
|
|
811
817
|
// ============================================================================
|
|
812
818
|
// EVENTS TAB (Swimlane timeline)
|
|
813
819
|
// ============================================================================
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,11 @@ export * from './components/audit'
|
|
|
45
45
|
// resolve.
|
|
46
46
|
export * from './components/checks'
|
|
47
47
|
|
|
48
|
+
// Live issues queue (IssuesView — the grouped operational-issue triage queue,
|
|
49
|
+
// shared by OSS single-cluster and the hub fleet view; sibling to the Checks
|
|
50
|
+
// queue)
|
|
51
|
+
export * from './components/issues'
|
|
52
|
+
|
|
48
53
|
// Cluster switcher (shared trigger+dropdown for OSS Radar and Radar Hub)
|
|
49
54
|
export * from './components/cluster-switcher'
|
|
50
55
|
|
package/src/theme/components.css
CHANGED
|
@@ -65,6 +65,22 @@
|
|
|
65
65
|
box-shadow: var(--shadow-lg);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/* ── INLINE CODE ── */
|
|
69
|
+
|
|
70
|
+
.inline-code {
|
|
71
|
+
-webkit-box-decoration-break: clone;
|
|
72
|
+
box-decoration-break: clone;
|
|
73
|
+
border: 0.5px solid color-mix(in srgb, var(--color-brand) 24%, var(--border-default));
|
|
74
|
+
border-radius: 0.25rem;
|
|
75
|
+
background-color: color-mix(in srgb, var(--color-brand) 8%, var(--bg-elevated));
|
|
76
|
+
padding: 0.0625rem 0.3125rem;
|
|
77
|
+
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
|
|
78
|
+
font-size: 0.9em;
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
line-height: 1.5;
|
|
81
|
+
color: var(--text-primary);
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
/* ── BADGES ── */
|
|
69
85
|
|
|
70
86
|
.badge {
|
|
@@ -110,6 +126,25 @@
|
|
|
110
126
|
padding: 0.75rem;
|
|
111
127
|
}
|
|
112
128
|
|
|
129
|
+
/* ── ISSUE ROW DETAILS ── */
|
|
130
|
+
|
|
131
|
+
.issue-details-motion {
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-template-rows: 0fr;
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
transition: grid-template-rows 200ms ease-out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.issue-details-motion-open {
|
|
139
|
+
grid-template-rows: 1fr;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@starting-style {
|
|
143
|
+
.issue-details-motion-open {
|
|
144
|
+
grid-template-rows: 0fr;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
113
148
|
/* ── SUBTLE BORDERS ── */
|
|
114
149
|
|
|
115
150
|
.table-divide-subtle > * + * {
|
package/src/types/core.ts
CHANGED
|
@@ -411,7 +411,8 @@ export interface ResolvedEnvFromEntry {
|
|
|
411
411
|
values: Record<string, string>
|
|
412
412
|
isSecret: boolean
|
|
413
413
|
}
|
|
414
|
-
export type
|
|
414
|
+
export type ResolvedEnvFromKey = `configmap:${string}` | `secret:${string}`
|
|
415
|
+
export type ResolvedEnvFrom = Partial<Record<ResolvedEnvFromKey, ResolvedEnvFromEntry>>
|
|
415
416
|
|
|
416
417
|
// Resource reference (for relationships)
|
|
417
418
|
export interface ResourceRef {
|
|
@@ -939,7 +940,7 @@ export interface TrafficFilters {
|
|
|
939
940
|
// Library consumers (Radar Hub) get all GitOps surfaces — the package
|
|
940
941
|
// IS the public surface, so adding new top-level views must extend
|
|
941
942
|
// this type rather than rely on app-local extensions.
|
|
942
|
-
export type ExtendedMainView = MainView | 'traffic' | 'cost' | 'audit' | 'gitops'
|
|
943
|
+
export type ExtendedMainView = MainView | 'traffic' | 'cost' | 'audit' | 'gitops' | 'issues'
|
|
943
944
|
|
|
944
945
|
// ============================================================================
|
|
945
946
|
// Image Filesystem Types
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ResourceRef } from '../types'
|
|
2
|
+
|
|
3
|
+
export function replicaScalers(scalers?: ResourceRef[]): ResourceRef[] | undefined {
|
|
4
|
+
const result = scalers?.filter((ref) => {
|
|
5
|
+
const kind = ref.kind.toLowerCase()
|
|
6
|
+
return kind === 'horizontalpodautoscaler' || kind === 'scaledobject'
|
|
7
|
+
})
|
|
8
|
+
return result && result.length > 0 ? result : undefined
|
|
9
|
+
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { pluralize } from '../../utils/pluralize'
|
|
3
|
-
import type { SelectedKindInfo } from './ResourcesSidebar'
|
|
4
|
-
|
|
5
|
-
// Mirrors the empty-state hint rule inside ResourcesView (kept in
|
|
6
|
-
// sync by reuse) — pure so we can pin the lookup + render contract
|
|
7
|
-
// without rendering the React component. See
|
|
8
|
-
// `packages/k8s-ui/src/components/resources/ResourcesView.tsx`
|
|
9
|
-
// search for "the count is unfiltered".
|
|
10
|
-
function sidebarHint(
|
|
11
|
-
counts: Record<string, number>,
|
|
12
|
-
selectedKind: SelectedKindInfo,
|
|
13
|
-
searchTerm: string,
|
|
14
|
-
): string | null {
|
|
15
|
-
if (!searchTerm) return null
|
|
16
|
-
const key = selectedKind.group ? `${selectedKind.group}/${selectedKind.kind}` : selectedKind.kind
|
|
17
|
-
const totalForKind = counts[key] ?? 0
|
|
18
|
-
if (totalForKind === 0) return null
|
|
19
|
-
return `The sidebar shows ${pluralize(totalForKind, selectedKind.kind)} in the cluster — the count is unfiltered.`
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('ResourcesView empty-search sidebar hint (SKY-828 bug 46)', () => {
|
|
23
|
-
// The bug: sidebar count badge stays unfiltered and shows the
|
|
24
|
-
// cluster-wide total per kind. When a search returns zero rows
|
|
25
|
-
// (e.g. "5 Pods" in sidebar, "No Pods found" in pane) the user
|
|
26
|
-
// reads the badge as a lie. The empty-state appends a sentence
|
|
27
|
-
// making the unfiltered nature explicit. The rule must:
|
|
28
|
-
// 1. Suppress entirely when there's no active search.
|
|
29
|
-
// 2. Suppress when the cluster total for the kind is 0
|
|
30
|
-
// (no badge to explain → no hint).
|
|
31
|
-
// 3. Look up the count under `${group}/${kind}` for grouped
|
|
32
|
-
// kinds, plain `kind` for core kinds (matches sidebar key).
|
|
33
|
-
// 4. Pluralize via pluralize() so "Ingress" → "Ingresses",
|
|
34
|
-
// "NetworkPolicy" → "NetworkPolicies" (regression of the
|
|
35
|
-
// naive `${kind}s` Cursor Bugbot caught in 80eb64b).
|
|
36
|
-
// 5. Singular for n===1: "1 Pod", not "1 Pods".
|
|
37
|
-
|
|
38
|
-
const pod: SelectedKindInfo = { name: 'pods', kind: 'Pod', group: '' }
|
|
39
|
-
|
|
40
|
-
it('returns null when there is no active search', () => {
|
|
41
|
-
expect(sidebarHint({ Pod: 232 }, pod, '')).toBeNull()
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('returns null when the cluster total for the kind is 0', () => {
|
|
45
|
-
expect(sidebarHint({ Pod: 0 }, pod, 'xyz')).toBeNull()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('returns null when the kind is missing from counts', () => {
|
|
49
|
-
expect(sidebarHint({}, pod, 'xyz')).toBeNull()
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('formats the sentence with the cluster total and pluralized kind', () => {
|
|
53
|
-
expect(sidebarHint({ Pod: 232 }, pod, 'xyz')).toBe(
|
|
54
|
-
'The sidebar shows 232 Pods in the cluster — the count is unfiltered.',
|
|
55
|
-
)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('uses singular noun when total is 1', () => {
|
|
59
|
-
expect(sidebarHint({ Pod: 1 }, pod, 'xyz')).toBe(
|
|
60
|
-
'The sidebar shows 1 Pod in the cluster — the count is unfiltered.',
|
|
61
|
-
)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('looks up grouped kinds under `${group}/${kind}`', () => {
|
|
65
|
-
const ar: SelectedKindInfo = { name: 'rollouts', kind: 'Rollout', group: 'argoproj.io' }
|
|
66
|
-
const counts = { Rollout: 999, 'argoproj.io/Rollout': 4 }
|
|
67
|
-
expect(sidebarHint(counts, ar, 'xyz')).toBe(
|
|
68
|
-
'The sidebar shows 4 Rollouts in the cluster — the count is unfiltered.',
|
|
69
|
-
)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('pluralizes Ingress correctly (regression: not "Ingresss")', () => {
|
|
73
|
-
const ing: SelectedKindInfo = { name: 'ingresses', kind: 'Ingress', group: '' }
|
|
74
|
-
expect(sidebarHint({ Ingress: 3 }, ing, 'xyz')).toBe(
|
|
75
|
-
'The sidebar shows 3 Ingresses in the cluster — the count is unfiltered.',
|
|
76
|
-
)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('pluralizes NetworkPolicy correctly (regression: not "NetworkPolicys")', () => {
|
|
80
|
-
const np: SelectedKindInfo = { name: 'networkpolicies', kind: 'NetworkPolicy', group: 'networking.k8s.io' }
|
|
81
|
-
expect(sidebarHint({ 'networking.k8s.io/NetworkPolicy': 2 }, np, 'xyz')).toBe(
|
|
82
|
-
'The sidebar shows 2 NetworkPolicies in the cluster — the count is unfiltered.',
|
|
83
|
-
)
|
|
84
|
-
})
|
|
85
|
-
})
|