@skyhook-io/k8s-ui 1.8.9 → 1.8.10
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 +1 -1
- package/src/components/applications/ApplicationsList.tsx +4 -1
- package/src/components/applications/ApplicationsView.tsx +40 -9
- package/src/components/checks/ChecksView.tsx +80 -49
- package/src/components/checks/severity.ts +29 -39
- package/src/components/gitops/GitOpsTableView.tsx +11 -56
- package/src/components/issues/IssuesView.tsx +186 -123
- package/src/components/issues/ResourceIssuesSection.test.tsx +39 -0
- package/src/components/issues/ResourceIssuesSection.tsx +21 -3
- package/src/components/issues/index.ts +0 -2
- package/src/components/issues/issues.test.ts +45 -4
- package/src/components/issues/severity.ts +27 -32
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -0
- package/src/components/resources/ResourcesSidebar.tsx +24 -6
- package/src/components/resources/ResourcesView.tsx +10 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
- package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
- package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -70
- package/src/components/ui/BoardSkeleton.tsx +47 -0
- package/src/components/ui/CardSection.tsx +117 -0
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/severity-tone.ts +56 -0
- package/src/theme/variables.css +14 -0
- package/src/utils/api-resources.test.ts +54 -0
- package/src/utils/api-resources.ts +68 -1
- package/tsconfig.json +2 -2
package/package.json
CHANGED
|
@@ -13,9 +13,11 @@ export interface ApplicationsListProps {
|
|
|
13
13
|
onSelect: (key: string) => void
|
|
14
14
|
/** Leading element in the header actions (e.g. a freshness control). */
|
|
15
15
|
headerActions?: ReactNode
|
|
16
|
+
/** First fetch in flight — chassis renders its shape-stable skeleton. */
|
|
17
|
+
loading?: boolean
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export function ApplicationsList({ apps, onSelect, headerActions }: ApplicationsListProps) {
|
|
20
|
+
export function ApplicationsList({ apps, onSelect, headerActions, loading }: ApplicationsListProps) {
|
|
19
21
|
// Env tokens this CLUSTER proved (identity classifications on the wire) feed
|
|
20
22
|
// the namespace heuristic, so sibling-less rows in discovered env namespaces
|
|
21
23
|
// still label without any hardcoded vocabulary.
|
|
@@ -24,6 +26,7 @@ export function ApplicationsList({ apps, onSelect, headerActions }: Applications
|
|
|
24
26
|
|
|
25
27
|
return (
|
|
26
28
|
<ApplicationsView
|
|
29
|
+
loading={loading}
|
|
27
30
|
variant="single"
|
|
28
31
|
entries={entries}
|
|
29
32
|
onSelect={onSelect}
|
|
@@ -5,6 +5,7 @@ import { clsx } from 'clsx'
|
|
|
5
5
|
import { StatusDot, mapHealthToTone } from '../ui/status-tone'
|
|
6
6
|
import { Tooltip } from '../ui/Tooltip'
|
|
7
7
|
import { EmptyState } from '../ui/EmptyState'
|
|
8
|
+
import { BoardTableSkeleton, BoardRailSkeleton } from '../ui/BoardSkeleton'
|
|
8
9
|
import { SearchBox } from '../ui/SearchBox'
|
|
9
10
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
10
11
|
import { PageHeader } from '../ui/PageHeader'
|
|
@@ -100,6 +101,9 @@ export interface ApplicationsViewProps {
|
|
|
100
101
|
emptySlot?: ReactNode
|
|
101
102
|
/** Leading element in the header actions cluster (e.g. a freshness control). */
|
|
102
103
|
headerActions?: ReactNode
|
|
104
|
+
/** First fetch in flight — render the shape-stable skeleton (pulsing tiles,
|
|
105
|
+
* rail stubs, table rows) instead of collapsing the chassis. */
|
|
106
|
+
loading?: boolean
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
const APPS_FILTER_SCHEMA = defineFilterSchema({
|
|
@@ -112,7 +116,7 @@ const APPS_FILTER_SCHEMA = defineFilterSchema({
|
|
|
112
116
|
system: { param: 'system', type: 'boolean' },
|
|
113
117
|
})
|
|
114
118
|
|
|
115
|
-
export function ApplicationsView({ entries: allEntries, variant, onSelect, title = 'Applications', description, emptySlot, headerActions }: ApplicationsViewProps) {
|
|
119
|
+
export function ApplicationsView({ entries: allEntries, variant, onSelect, title = 'Applications', description, emptySlot, headerActions, loading }: ApplicationsViewProps) {
|
|
116
120
|
// Facets + search + show-system live in the URL (shareable, bookmarkable) via
|
|
117
121
|
// the shared filter-state contract. Sort stays local: it's a compound
|
|
118
122
|
// {key, dir} view-preference, not a result-narrowing filter, so it isn't
|
|
@@ -274,9 +278,16 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
274
278
|
.sort((a, b) => (envRank(b[0] === 'none' ? undefined : b[0]) ?? -1) - (envRank(a[0] === 'none' ? undefined : a[0]) ?? -1))
|
|
275
279
|
.map(([env, count]) => ({ value: env, label: env === 'none' ? 'unlabeled' : env, count }))
|
|
276
280
|
|
|
281
|
+
// First fetch, nothing to show yet: drive the shape-stable skeleton. The
|
|
282
|
+
// loaded state hides zero-count health tiles, so without this the header
|
|
283
|
+
// (tiles + distribution bar), rail, and table would all pop in at once.
|
|
284
|
+
const initialLoading = Boolean(loading) && allEntries.length === 0
|
|
285
|
+
|
|
277
286
|
// Clickable status tile wired to the health facet — tap to filter to that tier.
|
|
278
287
|
const healthTile = (h: AppHealth, tone: SummaryTone) =>
|
|
279
|
-
|
|
288
|
+
initialLoading && (h === 'healthy' || h === 'degraded' || h === 'unhealthy') ? (
|
|
289
|
+
<SummaryTile key={h} label={HEALTH_META[h].label} value={0} tone={tone} loading />
|
|
290
|
+
) : counts.health[h] ? (
|
|
280
291
|
<SummaryTile key={h} label={HEALTH_META[h].label} value={counts.health[h]} tone={tone} active={fHealth.has(h)} onClick={() => filters.toggle('health', h)} />
|
|
281
292
|
) : null
|
|
282
293
|
|
|
@@ -301,7 +312,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
301
312
|
actions={
|
|
302
313
|
<>
|
|
303
314
|
{headerActions}
|
|
304
|
-
<SummaryTile label={total === 1 ? 'application' : 'applications'} value={total} />
|
|
315
|
+
<SummaryTile label={total === 1 ? 'application' : 'applications'} value={total} loading={initialLoading} />
|
|
305
316
|
{healthTile('unhealthy', 'error')}
|
|
306
317
|
{healthTile('degraded', 'warning')}
|
|
307
318
|
{healthTile('healthy', 'success')}
|
|
@@ -310,11 +321,15 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
310
321
|
</>
|
|
311
322
|
}
|
|
312
323
|
/>
|
|
313
|
-
|
|
314
|
-
className="mt-3"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
{initialLoading ? (
|
|
325
|
+
<div className="mt-3 h-1.5 w-full animate-pulse rounded-full bg-theme-hover" aria-hidden />
|
|
326
|
+
) : (
|
|
327
|
+
<DistributionBar
|
|
328
|
+
className="mt-3"
|
|
329
|
+
ariaLabel="Health distribution"
|
|
330
|
+
segments={HEALTH_ORDER.map((h) => ({ key: h, count: counts.health[h] ?? 0, fillClass: HEALTH_META[h].bar }))}
|
|
331
|
+
/>
|
|
332
|
+
)}
|
|
318
333
|
</div>
|
|
319
334
|
|
|
320
335
|
{/* Body: filter sidebar | content (toolbar + table). */}
|
|
@@ -330,6 +345,18 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
330
345
|
)}
|
|
331
346
|
</div>
|
|
332
347
|
<div className="flex-1 overflow-y-auto">
|
|
348
|
+
{initialLoading ? (
|
|
349
|
+
<BoardRailSkeleton
|
|
350
|
+
sections={[
|
|
351
|
+
['Availability', 5],
|
|
352
|
+
['Class', 3],
|
|
353
|
+
['Type', 4],
|
|
354
|
+
['Environment', 3],
|
|
355
|
+
['Source', 3],
|
|
356
|
+
]}
|
|
357
|
+
/>
|
|
358
|
+
) : (
|
|
359
|
+
<>
|
|
333
360
|
<Facet icon={HeartPulse} title="Availability" options={HEALTH_ORDER.map((h) => ({ value: h, label: HEALTH_META[h].label, count: counts.health[h] ?? 0, tone: HEALTH_TONE[h] }))} selected={fHealth} onToggle={(v) => filters.toggle('health', v)} />
|
|
334
361
|
<Facet icon={Layers} title="Class" options={CLASS_ORDER.map((c) => ({ value: c, label: CLASS_META[c].label, count: counts.workloadClass[c] ?? 0 }))} selected={fClass} onToggle={(v) => filters.toggle('class', v)} />
|
|
335
362
|
<Facet icon={Shapes} title="Type" options={CATEGORY_ORDER.map((c) => ({ value: c, label: CATEGORY_META[c].label, count: counts.category[c] ?? 0, tooltip: CATEGORY_META[c].tooltip }))} selected={fType} onToggle={(v) => filters.toggle('type', v)} />
|
|
@@ -342,6 +369,8 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
342
369
|
<span className="ml-auto tabular-nums text-theme-text-tertiary">{systemCount}</span>
|
|
343
370
|
</label>
|
|
344
371
|
)}
|
|
372
|
+
</>
|
|
373
|
+
)}
|
|
345
374
|
</div>
|
|
346
375
|
</aside>
|
|
347
376
|
|
|
@@ -369,7 +398,9 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
369
398
|
</div>
|
|
370
399
|
|
|
371
400
|
<div className="min-w-0 flex-1 overflow-auto bg-theme-base">
|
|
372
|
-
{
|
|
401
|
+
{initialLoading ? (
|
|
402
|
+
<BoardTableSkeleton />
|
|
403
|
+
) : entries.length === 0 ? (
|
|
373
404
|
emptySlot && allEntries.length === 0 ? (
|
|
374
405
|
emptySlot
|
|
375
406
|
) : (
|
|
@@ -1,21 +1,40 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import { ChevronDown, ChevronRight, ExternalLink, EyeOff, MoreHorizontal, Search, ShieldCheck, Wrench, X } from 'lucide-react'
|
|
4
|
-
import { ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip } from '../ui'
|
|
3
|
+
import { AlertCircle, AlertOctagon, AlertTriangle, ChevronDown, ChevronRight, ExternalLink, EyeOff, Info, Layers, MoreHorizontal, Search, ShieldCheck, Wrench, X } from 'lucide-react'
|
|
4
|
+
import { CardBody, CardSection, ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip, NEUTRAL_CHIP_CLASS, renderProse } from '../ui'
|
|
5
5
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
6
6
|
import type { CheckMeta, CheckReference } from '../audit'
|
|
7
7
|
import { CHECK_SEVERITIES, CHECK_SEVERITY_RANK, type Check, type CheckSeverity, type EffectiveCheckFinding, type CheckResourceRef } from './types'
|
|
8
8
|
import {
|
|
9
9
|
SEVERITY_BADGE_CLASS,
|
|
10
10
|
SEVERITY_FILL_CLASS,
|
|
11
|
+
SEVERITY_HEADER_BAND_CLASS,
|
|
11
12
|
SEVERITY_LABEL,
|
|
12
13
|
SEVERITY_RAIL_CLASS,
|
|
14
|
+
SEVERITY_SOLID_CLASS,
|
|
13
15
|
SEVERITY_TEXT_CLASS,
|
|
14
|
-
categoryBadgeClass,
|
|
15
16
|
} from './severity'
|
|
16
17
|
|
|
17
18
|
const CATEGORIES: readonly string[] = ['Security', 'Reliability', 'Efficiency']
|
|
18
19
|
|
|
20
|
+
// Leading severity glyph, one per tier of the 4-tier ladder: critical = octagon,
|
|
21
|
+
// high = triangle, medium = circle, low = info.
|
|
22
|
+
const CHECK_SEVERITY_ICON: Record<CheckSeverity, ComponentType<{ className?: string }>> = {
|
|
23
|
+
critical: AlertOctagon,
|
|
24
|
+
high: AlertTriangle,
|
|
25
|
+
medium: AlertCircle,
|
|
26
|
+
low: Info,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// An out-of-contract severity the backend might emit is coerced to this tier
|
|
30
|
+
// once, up front (normalizeCheckSeverity), so the icon AND every color map
|
|
31
|
+
// (text/rail/band/pill) resolve together — a raw miss would crash the icon and
|
|
32
|
+
// silently drop the tint everywhere else.
|
|
33
|
+
const CHECK_SEVERITY_FALLBACK: CheckSeverity = 'medium'
|
|
34
|
+
// Object.hasOwn (not `in`) so inherited keys like "toString" don't slip past.
|
|
35
|
+
const normalizeCheckSeverity = (s: CheckSeverity): CheckSeverity =>
|
|
36
|
+
Object.hasOwn(CHECK_SEVERITY_ICON, s) ? s : CHECK_SEVERITY_FALLBACK
|
|
37
|
+
|
|
19
38
|
// Affected-resources shown inline before "View all". A check can fail on
|
|
20
39
|
// thousands of resources; the card stays scannable and only the rare big-list
|
|
21
40
|
// case pays the cost of a full expand.
|
|
@@ -410,17 +429,22 @@ export interface CheckRemediationBlockProps {
|
|
|
410
429
|
layout?: 'columns' | 'stack'
|
|
411
430
|
}
|
|
412
431
|
|
|
432
|
+
// Remediation body: WHY IT MATTERS (info, the description) → HOW TO FIX
|
|
433
|
+
// (wrench/emerald, the remediation + doc links), rendered as icon-led sections
|
|
434
|
+
// for every host. Prose runs through renderProse so `inline-code` spans become
|
|
435
|
+
// mono chips when the catalog copy carries them. The 'stack' layout is the
|
|
436
|
+
// compact single-column variant for narrow hosts.
|
|
413
437
|
export function CheckRemediationBlock({ description, remediation, references, layout = 'columns' }: CheckRemediationBlockProps) {
|
|
414
438
|
if (!description && !remediation && (!references || references.length === 0)) return null
|
|
415
439
|
|
|
416
440
|
if (layout === 'stack') {
|
|
417
441
|
return (
|
|
418
442
|
<div className="flex flex-col gap-2">
|
|
419
|
-
{description && <p className="text-[13px] leading-relaxed text-theme-text-secondary">{description}</p>}
|
|
443
|
+
{description && <p className="text-[13px] leading-relaxed text-theme-text-secondary">{renderProse(description)}</p>}
|
|
420
444
|
{remediation && (
|
|
421
445
|
<div>
|
|
422
446
|
<div className="text-[11px] uppercase tracking-wider text-theme-text-tertiary">How to fix</div>
|
|
423
|
-
<p className="mt-0.5 text-[13px] leading-relaxed text-theme-text-secondary">{remediation}</p>
|
|
447
|
+
<p className="mt-0.5 text-[13px] leading-relaxed text-theme-text-secondary">{renderProse(remediation)}</p>
|
|
424
448
|
</div>
|
|
425
449
|
)}
|
|
426
450
|
{references && references.length > 0 && <CheckReferenceLinks references={references} />}
|
|
@@ -429,25 +453,19 @@ export function CheckRemediationBlock({ description, remediation, references, la
|
|
|
429
453
|
}
|
|
430
454
|
|
|
431
455
|
return (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
{
|
|
435
|
-
<
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
</
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
<p className="text-sm leading-relaxed text-theme-text-secondary">{description}</p>
|
|
446
|
-
</section>
|
|
447
|
-
)}
|
|
448
|
-
</div>
|
|
449
|
-
{references && references.length > 0 && <CheckReferenceLinks references={references} />}
|
|
450
|
-
</>
|
|
456
|
+
<div className="flex flex-col divide-y divide-theme-border/70 [&>*]:py-4 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0">
|
|
457
|
+
{description && (
|
|
458
|
+
<CardSection icon={Info} label="Why it matters" tone="neutral">
|
|
459
|
+
<CardBody>{renderProse(description)}</CardBody>
|
|
460
|
+
</CardSection>
|
|
461
|
+
)}
|
|
462
|
+
{(remediation || (references && references.length > 0)) && (
|
|
463
|
+
<CardSection icon={Wrench} label="How to fix" tone="fix">
|
|
464
|
+
{remediation && <CardBody>{renderProse(remediation)}</CardBody>}
|
|
465
|
+
{references && references.length > 0 && <CheckReferenceLinks references={references} />}
|
|
466
|
+
</CardSection>
|
|
467
|
+
)}
|
|
468
|
+
</div>
|
|
451
469
|
)
|
|
452
470
|
}
|
|
453
471
|
|
|
@@ -500,16 +518,27 @@ export function CheckCardShell({
|
|
|
500
518
|
dimmed,
|
|
501
519
|
}: CheckCardShellProps) {
|
|
502
520
|
const Container = as
|
|
521
|
+
const sev = normalizeCheckSeverity(severity)
|
|
522
|
+
const SeverityIcon = CHECK_SEVERITY_ICON[sev]
|
|
503
523
|
return (
|
|
504
524
|
<Container
|
|
505
525
|
className={[
|
|
506
|
-
'overflow-hidden rounded-xl border
|
|
526
|
+
'overflow-hidden rounded-xl border bg-theme-surface transition-[border-color,box-shadow] duration-200',
|
|
527
|
+
// The open card lifts via elevation — heavier shadow + a bright
|
|
528
|
+
// emphasis edge that clearly separates it from sibling cards. Severity
|
|
529
|
+
// stays rationed to the band + pill; separation is depth, not more color.
|
|
530
|
+
// ring-1 widens the edge to 2px without the layout shift a border-2
|
|
531
|
+
// swap would cause on expand.
|
|
532
|
+
open ? 'border-[var(--border-emphasis)] ring-1 ring-[var(--border-emphasis)] shadow-theme-md' : 'border-theme-border shadow-theme-sm',
|
|
507
533
|
dimmed ? 'opacity-60' : '',
|
|
508
534
|
className ?? '',
|
|
509
535
|
]
|
|
510
536
|
.filter(Boolean)
|
|
511
537
|
.join(' ')}
|
|
512
538
|
>
|
|
539
|
+
{/* Leading severity icon is the at-a-glance cue; a trailing chevron shows
|
|
540
|
+
open/closed. Collapsed: neutral row + rail. Expanded: severity-tinted
|
|
541
|
+
band + solid pill — the tint is a focus signal, not per-row alarm. */}
|
|
513
542
|
<div
|
|
514
543
|
role="button"
|
|
515
544
|
tabIndex={0}
|
|
@@ -522,31 +551,33 @@ export function CheckCardShell({
|
|
|
522
551
|
onToggle()
|
|
523
552
|
}
|
|
524
553
|
}}
|
|
525
|
-
className={`group flex cursor-pointer items-
|
|
554
|
+
className={`group flex cursor-pointer items-center gap-3 border-l-[3px] py-3 pl-3 pr-4 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-radar-accent)]/40 ${open ? SEVERITY_HEADER_BAND_CLASS[sev] : SEVERITY_RAIL_CLASS[sev]}`}
|
|
526
555
|
>
|
|
527
|
-
<
|
|
556
|
+
<SeverityIcon className={`h-[18px] w-[18px] shrink-0 ${SEVERITY_TEXT_CLASS[sev]}`} aria-hidden />
|
|
528
557
|
|
|
529
558
|
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
530
559
|
<div className="flex flex-wrap items-center gap-2">
|
|
531
560
|
<span className="truncate text-sm font-semibold text-theme-text-primary">{title}</span>
|
|
532
|
-
<span className={`
|
|
561
|
+
<span className={`shrink-0 ${NEUTRAL_CHIP_CLASS}`}>{category}</span>
|
|
533
562
|
</div>
|
|
534
563
|
{description}
|
|
535
564
|
{summary}
|
|
536
565
|
</div>
|
|
537
566
|
|
|
538
|
-
<span className={`badge-sm
|
|
539
|
-
{SEVERITY_LABEL[
|
|
567
|
+
<span className={`badge-sm shrink-0 px-2.5 py-0.5 text-xs font-semibold ${open ? SEVERITY_SOLID_CLASS[sev] : SEVERITY_BADGE_CLASS[sev]}`}>
|
|
568
|
+
{SEVERITY_LABEL[sev]}
|
|
540
569
|
</span>
|
|
541
570
|
{renderActions?.()}
|
|
571
|
+
<ChevronRight className={`h-4 w-4 shrink-0 text-theme-text-tertiary transition-transform duration-200 ${open ? 'rotate-90' : ''}`} />
|
|
542
572
|
</div>
|
|
543
573
|
|
|
544
574
|
{/* Kept mounted (not `open &&`) so the grid-rows transition animates the
|
|
545
575
|
collapse too, matching IssueRow; inert when closed so SR + tab skip
|
|
546
|
-
the clipped content.
|
|
576
|
+
the clipped content. Body sits on the card surface (not a recessed grey
|
|
577
|
+
panel) so its text keeps enough contrast. */}
|
|
547
578
|
<div className="grid transition-[grid-template-rows] duration-200 ease-out" style={{ gridTemplateRows: open ? '1fr' : '0fr' }}>
|
|
548
579
|
<div className="overflow-hidden" inert={!open || undefined}>
|
|
549
|
-
<div className="flex flex-col
|
|
580
|
+
<div className="flex flex-col divide-y divide-theme-border/70 border-t border-theme-border bg-theme-surface py-4 pl-6 pr-4 [&>*]:py-4 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0">{children}</div>
|
|
550
581
|
</div>
|
|
551
582
|
</div>
|
|
552
583
|
</Container>
|
|
@@ -561,7 +592,9 @@ export interface CheckClusterBreakdownGroup {
|
|
|
561
592
|
}
|
|
562
593
|
|
|
563
594
|
export interface CheckClusterBreakdownShellProps<T extends CheckClusterBreakdownGroup> {
|
|
564
|
-
heading
|
|
595
|
+
/** Optional section heading. Omit when the caller already provides one (e.g.
|
|
596
|
+
* a wrapping CardSection owns the "Affected resources" eyebrow). */
|
|
597
|
+
heading?: ReactNode
|
|
565
598
|
groups: T[]
|
|
566
599
|
renderGroupBody: (group: T) => ReactNode
|
|
567
600
|
clusterCap?: number
|
|
@@ -584,7 +617,7 @@ export function CheckClusterBreakdownShell<T extends CheckClusterBreakdownGroup>
|
|
|
584
617
|
|
|
585
618
|
return (
|
|
586
619
|
<section className="flex flex-col gap-1.5">
|
|
587
|
-
<h4 className="text-[11px] font-semibold uppercase tracking-
|
|
620
|
+
{heading ? <h4 className="text-[11px] font-semibold uppercase tracking-[0.06em] text-theme-text-tertiary">{heading}</h4> : null}
|
|
588
621
|
<ul className="flex flex-col gap-1">
|
|
589
622
|
{shown.map((group) => {
|
|
590
623
|
const isOpen = openClusters.has(group.id)
|
|
@@ -687,18 +720,17 @@ function FleetCheckRow({
|
|
|
687
720
|
>
|
|
688
721
|
<CheckRemediationBlock description={meta?.description} remediation={meta?.remediation} references={meta?.references} />
|
|
689
722
|
|
|
690
|
-
<
|
|
723
|
+
<CardSection
|
|
724
|
+
icon={Layers}
|
|
725
|
+
label="Affected resources"
|
|
726
|
+
labelExtra={single ? `· ${fc.totalResources}` : `· ${fc.totalResources} · ${clusterCount} clusters`}
|
|
727
|
+
>
|
|
691
728
|
{single ? (
|
|
692
|
-
<ResourceList
|
|
693
|
-
label={`Affected resources (${fc.totalResources})`}
|
|
694
|
-
check={fc.clusters[0]}
|
|
695
|
-
resourceHref={resourceHref}
|
|
696
|
-
onResourceClick={onResourceClick}
|
|
697
|
-
/>
|
|
729
|
+
<ResourceList check={fc.clusters[0]} resourceHref={resourceHref} onResourceClick={onResourceClick} />
|
|
698
730
|
) : (
|
|
699
731
|
<ClusterBreakdown fc={fc} clusterLabel={clusterLabel} resourceHref={resourceHref} onResourceClick={onResourceClick} />
|
|
700
732
|
)}
|
|
701
|
-
</
|
|
733
|
+
</CardSection>
|
|
702
734
|
</CheckCardShell>
|
|
703
735
|
)
|
|
704
736
|
}
|
|
@@ -720,11 +752,8 @@ function ClusterBreakdown({
|
|
|
720
752
|
}) {
|
|
721
753
|
return (
|
|
722
754
|
<CheckClusterBreakdownShell
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
Affected resources <span className="tabular-nums">({fc.totalResources})</span> · {fc.clusters.length} clusters
|
|
726
|
-
</>
|
|
727
|
-
}
|
|
755
|
+
// Heading omitted — the wrapping CardSection (Layers · "Affected
|
|
756
|
+
// resources") already owns the eyebrow.
|
|
728
757
|
groups={fc.clusters.map((c) => ({
|
|
729
758
|
id: c.subject.cluster_id,
|
|
730
759
|
label: <ClusterName name={clusterLabel?.(c) || c.subject.cluster_id} />,
|
|
@@ -822,7 +851,9 @@ function FindingLine({
|
|
|
822
851
|
{showMessage && <span className="ml-1 truncate text-xs text-theme-text-tertiary">{finding.message}</span>}
|
|
823
852
|
</>
|
|
824
853
|
)
|
|
825
|
-
|
|
854
|
+
// items-baseline so the smaller mono kind label shares a baseline with the
|
|
855
|
+
// larger resource name (their line-heights differ).
|
|
856
|
+
const cls = 'group/f flex w-full items-baseline gap-2 rounded-md px-2 py-1 text-left text-sm transition-colors hover:bg-theme-hover/60'
|
|
826
857
|
return (
|
|
827
858
|
<li>
|
|
828
859
|
{onResourceClick ? (
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import type { CheckSeverity } from './types'
|
|
2
2
|
import { BADGE_SEVERITY_COLORS as sev } from '../ui/Badge'
|
|
3
|
+
import {
|
|
4
|
+
TONE_FILL_CLASS,
|
|
5
|
+
TONE_HEADER_BAND_CLASS,
|
|
6
|
+
TONE_RAIL_CLASS,
|
|
7
|
+
TONE_SOLID_CLASS,
|
|
8
|
+
TONE_TEXT_CLASS,
|
|
9
|
+
type SeverityTone,
|
|
10
|
+
} from '../ui/severity-tone'
|
|
3
11
|
|
|
4
12
|
// The visual language for the 4-tier Checks severity ladder. One hue per tier:
|
|
5
13
|
// red=critical, orange=high, amber=medium, neutral=low — read the queue's left
|
|
6
|
-
// rail top-to-bottom and severity is obvious without reading a word.
|
|
14
|
+
// rail top-to-bottom and severity is obvious without reading a word. The actual
|
|
15
|
+
// color strings are shared with the Issue card via the tone module; here we only
|
|
16
|
+
// map each tier onto its tone.
|
|
17
|
+
const CHECK_SEVERITY_TONE: Record<CheckSeverity, SeverityTone> = {
|
|
18
|
+
critical: 'red',
|
|
19
|
+
high: 'orange',
|
|
20
|
+
medium: 'amber',
|
|
21
|
+
low: 'slate',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const byTone = <T,>(toneMap: Record<SeverityTone, T>): Record<CheckSeverity, T> => ({
|
|
25
|
+
critical: toneMap[CHECK_SEVERITY_TONE.critical],
|
|
26
|
+
high: toneMap[CHECK_SEVERITY_TONE.high],
|
|
27
|
+
medium: toneMap[CHECK_SEVERITY_TONE.medium],
|
|
28
|
+
low: toneMap[CHECK_SEVERITY_TONE.low],
|
|
29
|
+
})
|
|
7
30
|
|
|
8
31
|
export const SEVERITY_LABEL: Record<CheckSeverity, string> = {
|
|
9
32
|
critical: 'Critical',
|
|
@@ -23,41 +46,8 @@ export const SEVERITY_BADGE_CLASS: Record<CheckSeverity, string> = {
|
|
|
23
46
|
low: sev.neutral,
|
|
24
47
|
}
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
low: 'bg-slate-400',
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const SEVERITY_TEXT_CLASS: Record<CheckSeverity, string> = {
|
|
35
|
-
critical: 'text-red-600 dark:text-red-400',
|
|
36
|
-
high: 'text-orange-600 dark:text-orange-400',
|
|
37
|
-
medium: 'text-amber-600 dark:text-amber-400',
|
|
38
|
-
low: 'text-slate-500 dark:text-slate-400',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Left accent rail on a queue row — the scan-down severity cue. Pairs a colored
|
|
42
|
-
// 2px border with a faint severity-tinted background that deepens on hover.
|
|
43
|
-
export const SEVERITY_RAIL_CLASS: Record<CheckSeverity, string> = {
|
|
44
|
-
critical: 'border-l-red-500 hover:bg-red-50/40 dark:hover:bg-red-950/20',
|
|
45
|
-
high: 'border-l-orange-500 hover:bg-orange-50/40 dark:hover:bg-orange-950/20',
|
|
46
|
-
medium: 'border-l-amber-500 hover:bg-amber-50/30 dark:hover:bg-amber-950/15',
|
|
47
|
-
low: 'border-l-slate-300 dark:border-l-slate-600 hover:bg-theme-hover/40',
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Category accent — a quiet tag (severity is the loud one). Security is the
|
|
51
|
-
// headline beat, so it gets the most distinct hue.
|
|
52
|
-
const CATEGORY_BADGE_CLASS: Record<string, string> = {
|
|
53
|
-
Security: 'bg-violet-50 text-violet-700 ring-1 ring-violet-200 dark:bg-violet-950/40 dark:text-violet-300 dark:ring-violet-900',
|
|
54
|
-
Reliability: 'bg-sky-50 text-sky-700 ring-1 ring-sky-200 dark:bg-sky-950/40 dark:text-sky-300 dark:ring-sky-900',
|
|
55
|
-
Efficiency: 'bg-teal-50 text-teal-700 ring-1 ring-teal-200 dark:bg-teal-950/40 dark:text-teal-300 dark:ring-teal-900',
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function categoryBadgeClass(category: string): string {
|
|
59
|
-
return (
|
|
60
|
-
CATEGORY_BADGE_CLASS[category] ??
|
|
61
|
-
'bg-theme-elevated text-theme-text-secondary ring-1 ring-theme-border'
|
|
62
|
-
)
|
|
63
|
-
}
|
|
49
|
+
export const SEVERITY_FILL_CLASS = byTone(TONE_FILL_CLASS)
|
|
50
|
+
export const SEVERITY_TEXT_CLASS = byTone(TONE_TEXT_CLASS)
|
|
51
|
+
export const SEVERITY_RAIL_CLASS = byTone(TONE_RAIL_CLASS)
|
|
52
|
+
export const SEVERITY_SOLID_CLASS = byTone(TONE_SOLID_CLASS)
|
|
53
|
+
export const SEVERITY_HEADER_BAND_CLASS = byTone(TONE_HEADER_BAND_CLASS)
|
|
@@ -32,6 +32,7 @@ import { FacetSection, FacetButton } from '../ui/Facet'
|
|
|
32
32
|
import { SortableTh, TH_CLASS, type SortDir } from '../ui/SortableTh'
|
|
33
33
|
import { DistributionBar } from '../ui/DistributionBar'
|
|
34
34
|
import { RowActionMenu, type RowActionItem } from '../ui/RowActionMenu'
|
|
35
|
+
import { BoardTableSkeleton, BoardRailSkeleton } from '../ui/BoardSkeleton'
|
|
35
36
|
import { getGitOpsResourceStatus } from './detail-helpers'
|
|
36
37
|
import { isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
|
|
37
38
|
import { toggleSet } from './GitOpsGraphFilterRail'
|
|
@@ -766,7 +767,7 @@ export function GitOpsTableView({
|
|
|
766
767
|
{modeLabel(mode)} view is queued behind the application list.
|
|
767
768
|
</div>
|
|
768
769
|
) : initialLoading ? (
|
|
769
|
-
<
|
|
770
|
+
<BoardTableSkeleton />
|
|
770
771
|
) : error ? (
|
|
771
772
|
<div className="p-4 text-sm text-red-500">Failed to load GitOps applications: {error.message}</div>
|
|
772
773
|
) : filteredRows.length === 0 ? (
|
|
@@ -816,60 +817,6 @@ export function GitOpsTableView({
|
|
|
816
817
|
// GitOpsTableView's visual language and not generally useful elsewhere.
|
|
817
818
|
// =============================================================================
|
|
818
819
|
|
|
819
|
-
// Shape-stable loading stand-ins. Both mirror the loaded anatomy so data
|
|
820
|
-
// resolves in place: rows appear inside the table frame, facets inside the
|
|
821
|
-
// rail — no half-height rail dangling next to a centered spinner, no layout
|
|
822
|
-
// jump when the response lands.
|
|
823
|
-
function GitOpsTableSkeleton() {
|
|
824
|
-
return (
|
|
825
|
-
<div aria-live="polite" aria-label="Loading GitOps applications…" className="divide-y divide-theme-border-light">
|
|
826
|
-
{Array.from({ length: 10 }, (_, i) => (
|
|
827
|
-
<div key={i} className="flex items-center gap-6 px-4 py-3" style={{ opacity: 1 - i * 0.09 }}>
|
|
828
|
-
<div className="min-w-0 flex-[2] space-y-2">
|
|
829
|
-
<div className="h-4 w-2/3 animate-pulse rounded bg-theme-hover" />
|
|
830
|
-
<div className="h-3 w-1/2 animate-pulse rounded bg-theme-hover" />
|
|
831
|
-
</div>
|
|
832
|
-
<div className="hidden h-4 flex-1 animate-pulse rounded bg-theme-hover md:block" />
|
|
833
|
-
<div className="h-5 w-20 animate-pulse rounded-full bg-theme-hover" />
|
|
834
|
-
<div className="h-5 w-20 animate-pulse rounded-full bg-theme-hover" />
|
|
835
|
-
<div className="hidden h-4 flex-[1.5] animate-pulse rounded bg-theme-hover lg:block" />
|
|
836
|
-
</div>
|
|
837
|
-
))}
|
|
838
|
-
</div>
|
|
839
|
-
)
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function GitOpsSidebarSkeleton() {
|
|
843
|
-
// Section stubs sized like the loaded rail (Sync ~4 rows, Health ~5,
|
|
844
|
-
// Automation ~3, Projects/Namespaces a few each) so the rail keeps its
|
|
845
|
-
// height while counts are unknown — showing real facet buttons with "0"
|
|
846
|
-
// counts would be false zeros.
|
|
847
|
-
const sections: Array<[string, number]> = [
|
|
848
|
-
['Sync', 4],
|
|
849
|
-
['Health', 5],
|
|
850
|
-
['Automation', 3],
|
|
851
|
-
['Projects', 4],
|
|
852
|
-
['Namespaces', 4],
|
|
853
|
-
]
|
|
854
|
-
return (
|
|
855
|
-
<div aria-hidden>
|
|
856
|
-
{sections.map(([title, rows]) => (
|
|
857
|
-
<div key={title} className="border-b border-theme-border-light px-3 py-3">
|
|
858
|
-
<div className="mb-2 h-3 w-24 animate-pulse rounded bg-theme-hover" />
|
|
859
|
-
<div className="space-y-1.5">
|
|
860
|
-
{Array.from({ length: rows }, (_, i) => (
|
|
861
|
-
<div key={i} className="flex items-center justify-between px-1.5 py-1">
|
|
862
|
-
<div className="h-3.5 animate-pulse rounded bg-theme-hover" style={{ width: `${52 - i * 6}%` }} />
|
|
863
|
-
<div className="h-3.5 w-6 animate-pulse rounded bg-theme-hover" />
|
|
864
|
-
</div>
|
|
865
|
-
))}
|
|
866
|
-
</div>
|
|
867
|
-
</div>
|
|
868
|
-
))}
|
|
869
|
-
</div>
|
|
870
|
-
)
|
|
871
|
-
}
|
|
872
|
-
|
|
873
820
|
function GitOpsFilterSidebar({
|
|
874
821
|
loading,
|
|
875
822
|
side,
|
|
@@ -936,7 +883,15 @@ function GitOpsFilterSidebar({
|
|
|
936
883
|
</div>
|
|
937
884
|
<div className="flex-1 overflow-y-auto">
|
|
938
885
|
{loading ? (
|
|
939
|
-
<
|
|
886
|
+
<BoardRailSkeleton
|
|
887
|
+
sections={[
|
|
888
|
+
['Sync', 4],
|
|
889
|
+
['Health', 5],
|
|
890
|
+
['Automation', 3],
|
|
891
|
+
['Projects', 4],
|
|
892
|
+
['Namespaces', 4],
|
|
893
|
+
]}
|
|
894
|
+
/>
|
|
940
895
|
) : (
|
|
941
896
|
<>
|
|
942
897
|
{AVAILABLE_MODES.length > 1 && (
|