@skyhook-io/k8s-ui 1.14.6 → 1.14.7
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/ApplicationDetail.tsx +36 -4
- package/src/components/applications/ApplicationsView.tsx +2 -1
- package/src/components/audit/AuditAlerts.tsx +35 -37
- package/src/components/audit/AuditFindingsTable.tsx +23 -27
- package/src/components/charts/PrometheusChartsView.tsx +29 -10
- package/src/components/checks/ChecksView.tsx +56 -26
- package/src/components/gitops/GitOpsDetailLayout.tsx +12 -9
- package/src/components/issues/IssuesView.tsx +35 -54
- package/src/components/logs/LogCore.tsx +40 -9
- package/src/components/resources/ResourcesSidebar.tsx +66 -78
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +21 -15
- package/src/components/resources/renderers/CompositionRenderer.tsx +30 -10
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +16 -12
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/GenericRenderer.tsx +13 -7
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +6 -4
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +7 -4
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +13 -15
- package/src/components/resources/renderers/SbomReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +13 -6
- package/src/components/shared/CreateResourceDialog.tsx +7 -4
- package/src/components/timeline/TimelineList.tsx +14 -11
- package/src/components/timeline/TimelineStrip.tsx +26 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -5
- package/src/components/timeline/TimelineToolbar.tsx +43 -10
- package/src/components/topology/TopologyControls.tsx +27 -8
- package/src/components/topology/TopologyGraph.tsx +14 -5
- package/src/components/trace/ReachabilityView.tsx +17 -16
- package/src/components/ui/CardSection.tsx +79 -35
- package/src/components/ui/Collapse.test.tsx +74 -0
- package/src/components/ui/Collapse.tsx +115 -29
- package/src/components/ui/DialogPortal.tsx +7 -2
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +7 -4
- package/src/components/ui/RestrictedState.tsx +7 -4
- package/src/components/ui/RowActionMenu.tsx +20 -8
- package/src/components/ui/SelectMenu.tsx +18 -6
- package/src/components/ui/YamlEditor.tsx +7 -9
- package/src/components/ui/drawer-components.tsx +14 -21
- package/src/components/ui/index.ts +30 -22
- package/src/components/workload/WorkloadView.tsx +6 -10
- package/src/hooks/useAnimatedUnmount.ts +21 -3
- package/src/theme/components.css +4 -1
- package/src/utils/animation.ts +93 -13
package/package.json
CHANGED
|
@@ -27,6 +27,12 @@ import argoCdLogo from "../../assets/gitops/argocd.png";
|
|
|
27
27
|
import fluxLogo from "../../assets/gitops/flux.svg";
|
|
28
28
|
import { StatusDot, mapHealthToTone } from "../ui/status-tone";
|
|
29
29
|
import { Tooltip } from "../ui/Tooltip";
|
|
30
|
+
import { useAnimatedUnmount } from "../../hooks/useAnimatedUnmount";
|
|
31
|
+
import {
|
|
32
|
+
TRANSITION_MENU,
|
|
33
|
+
overlayExitMs,
|
|
34
|
+
overlayTransitionStyle,
|
|
35
|
+
} from "../../utils/animation";
|
|
30
36
|
import { EmptyState } from "../ui/EmptyState";
|
|
31
37
|
import { ResourceRefBadge } from "../ui/drawer-components";
|
|
32
38
|
import { TopologyGraph } from "../topology/TopologyGraph";
|
|
@@ -2884,6 +2890,10 @@ function ApplicationScopeSelector({
|
|
|
2884
2890
|
onFocus: (owner: WorkloadFocus) => void;
|
|
2885
2891
|
}) {
|
|
2886
2892
|
const [open, setOpen] = useState(false);
|
|
2893
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(
|
|
2894
|
+
open,
|
|
2895
|
+
overlayExitMs("menu"),
|
|
2896
|
+
);
|
|
2887
2897
|
const [query, setQuery] = useState("");
|
|
2888
2898
|
const rootRef = useDismissablePopover<HTMLDivElement>(open, setOpen);
|
|
2889
2899
|
const selectedKey = selectedWorkload ? workloadKey(selectedWorkload) : null;
|
|
@@ -2944,10 +2954,19 @@ function ApplicationScopeSelector({
|
|
|
2944
2954
|
aria-hidden
|
|
2945
2955
|
/>
|
|
2946
2956
|
</button>
|
|
2947
|
-
{
|
|
2957
|
+
{shouldRender && (
|
|
2948
2958
|
<div
|
|
2949
2959
|
role="listbox"
|
|
2950
|
-
|
|
2960
|
+
inert={!open}
|
|
2961
|
+
className={clsx(
|
|
2962
|
+
"absolute left-0 top-full z-50 mt-1 w-[min(32rem,calc(100vw-2rem))] origin-top-left overflow-hidden rounded-md border border-theme-border bg-theme-surface shadow-theme-md",
|
|
2963
|
+
TRANSITION_MENU,
|
|
2964
|
+
isOpen
|
|
2965
|
+
? "opacity-100 translate-y-0 scale-100"
|
|
2966
|
+
: "opacity-0 -translate-y-1 scale-[0.97]",
|
|
2967
|
+
!open && "pointer-events-none",
|
|
2968
|
+
)}
|
|
2969
|
+
style={overlayTransitionStyle(isOpen, "menu")}
|
|
2951
2970
|
onMouseLeave={() => onFocus(null)}
|
|
2952
2971
|
>
|
|
2953
2972
|
<div className="border-b border-theme-border p-1">
|
|
@@ -3371,6 +3390,10 @@ function EnvSwitcher({
|
|
|
3371
3390
|
onSwitch?: (appKey: string) => void;
|
|
3372
3391
|
}) {
|
|
3373
3392
|
const [open, setOpen] = useState(false);
|
|
3393
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(
|
|
3394
|
+
open,
|
|
3395
|
+
overlayExitMs("menu"),
|
|
3396
|
+
);
|
|
3374
3397
|
const rootRef = useDismissablePopover<HTMLDivElement>(open, setOpen);
|
|
3375
3398
|
|
|
3376
3399
|
const envInstances = useMemo(
|
|
@@ -3475,10 +3498,19 @@ function EnvSwitcher({
|
|
|
3475
3498
|
</button>
|
|
3476
3499
|
{evidenceChip}
|
|
3477
3500
|
{lagChip}
|
|
3478
|
-
{
|
|
3501
|
+
{shouldRender && (
|
|
3479
3502
|
<div
|
|
3480
3503
|
role="listbox"
|
|
3481
|
-
|
|
3504
|
+
inert={!open}
|
|
3505
|
+
className={clsx(
|
|
3506
|
+
"absolute left-0 top-full z-50 mt-1 max-h-80 w-80 origin-top-left overflow-y-auto rounded-md border border-theme-border bg-theme-surface p-1 shadow-theme-md",
|
|
3507
|
+
TRANSITION_MENU,
|
|
3508
|
+
isOpen
|
|
3509
|
+
? "opacity-100 translate-y-0 scale-100"
|
|
3510
|
+
: "opacity-0 -translate-y-1 scale-[0.97]",
|
|
3511
|
+
!open && "pointer-events-none",
|
|
3512
|
+
)}
|
|
3513
|
+
style={overlayTransitionStyle(isOpen, "menu")}
|
|
3482
3514
|
>
|
|
3483
3515
|
{instances.map((inst) => {
|
|
3484
3516
|
const isActive = inst.appKey === activeKey;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, useState, useEffect, useRef, useCallback } from 'react'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
import { ChevronRight, Layers, Boxes, HeartPulse, Shapes, Globe, Tag } from 'lucide-react'
|
|
4
|
+
import { CollapseChevron } from '../ui/Collapse'
|
|
4
5
|
import { clsx } from 'clsx'
|
|
5
6
|
import { StatusDot, mapHealthToTone } from '../ui/status-tone'
|
|
6
7
|
import { Tooltip } from '../ui/Tooltip'
|
|
@@ -497,7 +498,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
497
498
|
<Tooltip content={applicationRuntimeLabel(r.members.map((member) => member.row), r.workloadClass, r.health)} delay={150}>
|
|
498
499
|
<span className={clsx('h-8 w-1 shrink-0 rounded-full', HEALTH_META[r.health].bar)} />
|
|
499
500
|
</Tooltip>
|
|
500
|
-
<
|
|
501
|
+
<CollapseChevron open={r.expanded} className="h-3.5 w-3.5" />
|
|
501
502
|
<span className="truncate font-semibold text-theme-text-primary">{r.label}</span>
|
|
502
503
|
{r.workloadClass !== 'job' && <BatchSignalChip signal={firstBatchSignal(r.members.map((m) => m.row))} />}
|
|
503
504
|
<Tooltip
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import { ClipboardCheck,
|
|
2
|
+
import { ClipboardCheck, ShieldAlert, AlertTriangle, ArrowRight } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { BP_CATEGORY_BADGE, DEFAULT_BADGE_COLOR } from '../../utils/badge-colors'
|
|
5
5
|
import { SEVERITY_TEXT_CLASS } from '../checks/severity'
|
|
6
|
+
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse'
|
|
6
7
|
|
|
7
8
|
export interface AuditFinding {
|
|
8
9
|
kind: string
|
|
@@ -36,6 +37,7 @@ interface AuditAlertsProps {
|
|
|
36
37
|
*/
|
|
37
38
|
export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
|
|
38
39
|
const [expanded, setExpanded] = useState(false)
|
|
40
|
+
const { panelId, buttonProps } = useDisclosure(expanded)
|
|
39
41
|
|
|
40
42
|
if (findings.length === 0) return null
|
|
41
43
|
|
|
@@ -45,11 +47,12 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
|
|
|
45
47
|
return (
|
|
46
48
|
<section className="rounded-lg border border-theme-border bg-theme-surface p-4 shadow-theme-sm">
|
|
47
49
|
<button
|
|
50
|
+
{...buttonProps}
|
|
48
51
|
type="button"
|
|
49
52
|
onClick={() => setExpanded(!expanded)}
|
|
50
53
|
className="flex w-full items-center gap-2 rounded-md text-left transition-colors hover:text-theme-text-primary"
|
|
51
54
|
>
|
|
52
|
-
<
|
|
55
|
+
<CollapseChevron open={expanded} className="w-4 h-4" />
|
|
53
56
|
<ClipboardCheck className="w-4 h-4 text-theme-text-secondary" />
|
|
54
57
|
<span className="text-sm font-semibold text-theme-text-primary">Audit Findings</span>
|
|
55
58
|
<div className="flex items-center gap-2 ml-1">
|
|
@@ -62,43 +65,38 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
|
|
|
62
65
|
</div>
|
|
63
66
|
</button>
|
|
64
67
|
|
|
65
|
-
<
|
|
66
|
-
className="
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<span className="text-xs text-theme-text-secondary">{f.message}</span>
|
|
82
|
-
<span className={clsx('badge-sm text-[10px] shrink-0', BP_CATEGORY_BADGE[f.category] || DEFAULT_BADGE_COLOR)}>
|
|
83
|
-
{f.category}
|
|
84
|
-
</span>
|
|
85
|
-
</div>
|
|
68
|
+
<Collapse open={expanded} id={panelId}>
|
|
69
|
+
<div className="flex flex-col gap-0.5 pt-3">
|
|
70
|
+
{findings.map((f, i) => {
|
|
71
|
+
const isHigh = f.severity === 'danger'
|
|
72
|
+
return (
|
|
73
|
+
<div key={`${f.checkID}-${i}`} className="flex items-start gap-2 py-1">
|
|
74
|
+
{isHigh ? (
|
|
75
|
+
<ShieldAlert className={clsx('w-3.5 h-3.5 shrink-0 mt-0.5', SEVERITY_TEXT_CLASS.high)} />
|
|
76
|
+
) : (
|
|
77
|
+
<AlertTriangle className={clsx('w-3.5 h-3.5 shrink-0 mt-0.5', SEVERITY_TEXT_CLASS.medium)} />
|
|
78
|
+
)}
|
|
79
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1">
|
|
80
|
+
<span className="text-xs text-theme-text-secondary">{f.message}</span>
|
|
81
|
+
<span className={clsx('badge-sm text-[10px] shrink-0', BP_CATEGORY_BADGE[f.category] || DEFAULT_BADGE_COLOR)}>
|
|
82
|
+
{f.category}
|
|
83
|
+
</span>
|
|
86
84
|
</div>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
})}
|
|
88
|
+
{onViewAll && (
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
onClick={(e) => { e.stopPropagation(); onViewAll() }}
|
|
92
|
+
className="mt-1 flex items-center gap-1 py-1 text-xs text-accent-text transition-colors hover:text-accent"
|
|
93
|
+
>
|
|
94
|
+
View all findings
|
|
95
|
+
<ArrowRight className="w-3 h-3" />
|
|
96
|
+
</button>
|
|
97
|
+
)}
|
|
100
98
|
</div>
|
|
101
|
-
</
|
|
99
|
+
</Collapse>
|
|
102
100
|
</section>
|
|
103
101
|
)
|
|
104
102
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState, useMemo, useRef, useEffect, type Dispatch, type SetStateAction } from 'react'
|
|
2
|
-
import { ShieldAlert, AlertTriangle,
|
|
1
|
+
import { useState, useMemo, useRef, useEffect, useId, type Dispatch, type SetStateAction } from 'react'
|
|
2
|
+
import { ShieldAlert, AlertTriangle, CheckCircle2, ExternalLink, MoreHorizontal, EyeOff, Layers } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import type { AuditFinding } from './AuditAlerts'
|
|
5
5
|
import { BP_CATEGORY_BADGE, DEFAULT_BADGE_COLOR } from '../../utils/badge-colors'
|
|
@@ -8,6 +8,7 @@ import { SearchBox } from '../ui/SearchBox'
|
|
|
8
8
|
import { FilterPill } from '../ui/FilterPill'
|
|
9
9
|
import { pluralize } from '../../utils/pluralize'
|
|
10
10
|
import { SEVERITY_TEXT_CLASS } from '../checks/severity'
|
|
11
|
+
import { Collapse, CollapseChevron, useDisclosure, disclosurePanelId } from '../ui/Collapse'
|
|
11
12
|
|
|
12
13
|
const CATEGORIES = ['Security', 'Reliability', 'Efficiency'] as const
|
|
13
14
|
const RAW_SEVERITIES = ['danger', 'warning'] as const
|
|
@@ -68,6 +69,9 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
|
|
|
68
69
|
const [searchTerm, setSearchTerm] = useState('')
|
|
69
70
|
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
|
70
71
|
const [expandedNS, setExpandedNS] = useState<Set<string>>(new Set())
|
|
72
|
+
// Namespace rows are mapped inline, so they can't each call useDisclosure;
|
|
73
|
+
// one generated prefix plus the namespace keeps aria-controls unique.
|
|
74
|
+
const nsPanelBase = useId()
|
|
71
75
|
const [groupByNS, setGroupByNS] = useState(false)
|
|
72
76
|
|
|
73
77
|
const toggleInSet = (setter: Dispatch<SetStateAction<Set<string>>>, value: string) => {
|
|
@@ -328,6 +332,7 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
|
|
|
328
332
|
role="button"
|
|
329
333
|
tabIndex={0}
|
|
330
334
|
aria-expanded={nsExpanded}
|
|
335
|
+
aria-controls={disclosurePanelId(nsPanelBase, ns)}
|
|
331
336
|
onClick={() => toggleNS(ns)}
|
|
332
337
|
onKeyDown={(e) => {
|
|
333
338
|
if (e.target !== e.currentTarget) return
|
|
@@ -335,7 +340,7 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
|
|
|
335
340
|
}}
|
|
336
341
|
className="group flex items-center gap-3 w-full px-4 py-2 rounded-lg hover:bg-theme-hover/30 transition-colors text-left cursor-pointer focus-visible:ring-2 focus-visible:ring-theme-text-primary/20 focus-visible:outline-none"
|
|
337
342
|
>
|
|
338
|
-
<
|
|
343
|
+
<CollapseChevron open={nsExpanded} className="w-4 h-4" />
|
|
339
344
|
<span className="text-sm font-semibold text-theme-text-primary">{ns}</span>
|
|
340
345
|
<span className="text-xs text-theme-text-tertiary">{pluralize(nsGroups.length, 'resource')}</span>
|
|
341
346
|
<span className="flex-1" />
|
|
@@ -347,18 +352,13 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
|
|
|
347
352
|
<ContextMenu items={[{ label: `Hide ${ns} namespace`, onClick: () => onHideNamespace(ns) }]} />
|
|
348
353
|
)}
|
|
349
354
|
</div>
|
|
350
|
-
<
|
|
351
|
-
className="
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<div className="pl-4">
|
|
356
|
-
{nsGroups.map(g => (
|
|
357
|
-
<ResourceGroupRow key={`${g.kind}/${g.namespace}/${g.name}`} group={g} checks={checks} expanded={expanded} onToggle={toggle} onResourceClick={onResourceClick} onHideCheck={onHideCheck} onHideCategory={onHideCategory} />
|
|
358
|
-
))}
|
|
359
|
-
</div>
|
|
355
|
+
<Collapse open={nsExpanded} id={disclosurePanelId(nsPanelBase, ns)}>
|
|
356
|
+
<div className="pl-4">
|
|
357
|
+
{nsGroups.map(g => (
|
|
358
|
+
<ResourceGroupRow key={`${g.kind}/${g.namespace}/${g.name}`} group={g} checks={checks} expanded={expanded} onToggle={toggle} onResourceClick={onResourceClick} onHideCheck={onHideCheck} onHideCategory={onHideCategory} />
|
|
359
|
+
))}
|
|
360
360
|
</div>
|
|
361
|
-
</
|
|
361
|
+
</Collapse>
|
|
362
362
|
</div>
|
|
363
363
|
)
|
|
364
364
|
})}
|
|
@@ -441,13 +441,14 @@ function ResourceGroupRow({ group: g, checks, expanded, onToggle, onResourceClic
|
|
|
441
441
|
const key = `${g.kind}/${g.namespace}/${g.name}`
|
|
442
442
|
const isExpanded = expanded.has(key)
|
|
443
443
|
const hasHigh = g.danger > 0
|
|
444
|
+
const { panelId, buttonProps } = useDisclosure(isExpanded)
|
|
444
445
|
|
|
445
446
|
return (
|
|
446
447
|
<div>
|
|
447
448
|
<div
|
|
449
|
+
{...buttonProps}
|
|
448
450
|
role="button"
|
|
449
451
|
tabIndex={0}
|
|
450
|
-
aria-expanded={isExpanded}
|
|
451
452
|
onClick={() => onToggle(key)}
|
|
452
453
|
onKeyDown={(e) => {
|
|
453
454
|
if (e.target !== e.currentTarget) return
|
|
@@ -455,7 +456,7 @@ function ResourceGroupRow({ group: g, checks, expanded, onToggle, onResourceClic
|
|
|
455
456
|
}}
|
|
456
457
|
className="group flex items-center gap-3 w-full px-4 py-2.5 rounded-lg hover:bg-theme-hover/50 transition-colors text-left cursor-pointer focus-visible:ring-2 focus-visible:ring-theme-text-primary/20 focus-visible:outline-none"
|
|
457
458
|
>
|
|
458
|
-
<
|
|
459
|
+
<CollapseChevron open={isExpanded} className="w-3.5 h-3.5" />
|
|
459
460
|
{hasHigh ? (
|
|
460
461
|
<ShieldAlert className={clsx('w-4 h-4 shrink-0', SEVERITY_TEXT_CLASS.high)} />
|
|
461
462
|
) : (
|
|
@@ -485,18 +486,13 @@ function ResourceGroupRow({ group: g, checks, expanded, onToggle, onResourceClic
|
|
|
485
486
|
<ContextMenu items={[{ label: `Hide ${g.namespace} namespace`, onClick: () => onHideNamespace(g.namespace) }]} />
|
|
486
487
|
)}
|
|
487
488
|
</div>
|
|
488
|
-
<
|
|
489
|
-
className="
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
<div className="pl-11 pb-1">
|
|
494
|
-
{g.findings.map((f, i) => (
|
|
495
|
-
<FindingDetail key={`${f.checkID}-${i}`} finding={f} meta={checks?.[f.checkID]} onHideCheck={onHideCheck} onHideCategory={onHideCategory} />
|
|
496
|
-
))}
|
|
497
|
-
</div>
|
|
489
|
+
<Collapse open={isExpanded} id={panelId}>
|
|
490
|
+
<div className="pl-11 pb-1">
|
|
491
|
+
{g.findings.map((f, i) => (
|
|
492
|
+
<FindingDetail key={`${f.checkID}-${i}`} finding={f} meta={checks?.[f.checkID]} onHideCheck={onHideCheck} onHideCategory={onHideCategory} />
|
|
493
|
+
))}
|
|
498
494
|
</div>
|
|
499
|
-
</
|
|
495
|
+
</Collapse>
|
|
500
496
|
</div>
|
|
501
497
|
)
|
|
502
498
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import { clsx } from "clsx";
|
|
2
3
|
import { BarChart3, Wifi, WifiOff, Loader2 } from "lucide-react";
|
|
4
|
+
import { Collapse, CollapseChevron, useDisclosure } from "../ui/Collapse";
|
|
3
5
|
import { AreaChart } from "./AreaChart";
|
|
4
6
|
import { PaneLoader } from "../ui/PaneLoader";
|
|
5
7
|
import { MetricsSummary } from "./MetricsSummary";
|
|
@@ -339,19 +341,36 @@ export function PrometheusChartsView({
|
|
|
339
341
|
{metrics.hint}
|
|
340
342
|
</p>
|
|
341
343
|
)}
|
|
342
|
-
{metrics?.query &&
|
|
343
|
-
<details className="mt-3 w-full max-w-lg text-left">
|
|
344
|
-
<summary className="cursor-pointer text-xs text-theme-text-quaternary hover:text-theme-text-tertiary">
|
|
345
|
-
Diagnostics: show PromQL query
|
|
346
|
-
</summary>
|
|
347
|
-
<div className="mt-2 break-all rounded border border-theme-border bg-theme-base p-2 font-mono text-xs text-theme-text-secondary">
|
|
348
|
-
{metrics.query}
|
|
349
|
-
</div>
|
|
350
|
-
</details>
|
|
351
|
-
)}
|
|
344
|
+
{metrics?.query && <QueryDiagnostics query={metrics.query} />}
|
|
352
345
|
</div>
|
|
353
346
|
)}
|
|
354
347
|
</div>
|
|
355
348
|
</div>
|
|
356
349
|
);
|
|
357
350
|
}
|
|
351
|
+
|
|
352
|
+
// The PromQL behind an empty chart, for the reader who wants to run it
|
|
353
|
+
// themselves. Lives on the shared disclosure so it opens like every other
|
|
354
|
+
// fold in the app instead of the browser's default <details> jump.
|
|
355
|
+
function QueryDiagnostics({ query }: { query: string }) {
|
|
356
|
+
const [open, setOpen] = useState(false);
|
|
357
|
+
const { panelId, buttonProps } = useDisclosure(open);
|
|
358
|
+
return (
|
|
359
|
+
<div className="mt-3 w-full max-w-lg text-left">
|
|
360
|
+
<button
|
|
361
|
+
{...buttonProps}
|
|
362
|
+
type="button"
|
|
363
|
+
onClick={() => setOpen((v) => !v)}
|
|
364
|
+
className="flex items-center gap-1 text-xs text-theme-text-quaternary hover:text-theme-text-tertiary"
|
|
365
|
+
>
|
|
366
|
+
<CollapseChevron open={open} className="h-3 w-3" />
|
|
367
|
+
Diagnostics: show PromQL query
|
|
368
|
+
</button>
|
|
369
|
+
<Collapse open={open} id={panelId}>
|
|
370
|
+
<div className="mt-2 break-all rounded border border-theme-border bg-theme-base p-2 font-mono text-xs text-theme-text-secondary">
|
|
371
|
+
{query}
|
|
372
|
+
</div>
|
|
373
|
+
</Collapse>
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode } from 'react'
|
|
1
|
+
import { useEffect, useId, useMemo, useRef, useState, type ComponentType, type ReactNode } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import { AlertCircle, AlertOctagon, AlertTriangle, ChevronDown,
|
|
3
|
+
import { AlertCircle, AlertOctagon, AlertTriangle, ChevronDown, ExternalLink, EyeOff, Info, Layers, MoreHorizontal, Search, ShieldCheck, Wrench, X } from 'lucide-react'
|
|
4
4
|
import { AlertBanner, CardBody, CardSection, ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip, Input, NEUTRAL_CHIP_CLASS, renderProse } from '../ui'
|
|
5
|
+
import { Collapse, CollapseChevron, useDisclosure, disclosurePanelId } from '../ui/Collapse'
|
|
5
6
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
6
7
|
import type { CheckMeta, CheckReference } from '../audit'
|
|
7
8
|
import { CHECK_SEVERITIES, CHECK_SEVERITY_RANK, type Check, type CheckSeverity, type EffectiveCheckFinding, type CheckResourceRef } from './types'
|
|
@@ -14,6 +15,8 @@ import {
|
|
|
14
15
|
SEVERITY_SOLID_CLASS,
|
|
15
16
|
SEVERITY_TEXT_CLASS,
|
|
16
17
|
} from './severity'
|
|
18
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
19
|
+
import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
17
20
|
|
|
18
21
|
const CATEGORIES: readonly string[] = ['Security', 'Reliability', 'Efficiency']
|
|
19
22
|
|
|
@@ -537,6 +540,7 @@ export function CheckCardShell({
|
|
|
537
540
|
const Container = as
|
|
538
541
|
const sev = normalizeCheckSeverity(severity)
|
|
539
542
|
const SeverityIcon = CHECK_SEVERITY_ICON[sev]
|
|
543
|
+
const { panelId, buttonProps } = useDisclosure(open)
|
|
540
544
|
return (
|
|
541
545
|
<Container
|
|
542
546
|
className={[
|
|
@@ -557,9 +561,9 @@ export function CheckCardShell({
|
|
|
557
561
|
open/closed. Collapsed: neutral row + rail. Expanded: severity-tinted
|
|
558
562
|
band + solid pill — the tint is a focus signal, not per-row alarm. */}
|
|
559
563
|
<div
|
|
564
|
+
{...buttonProps}
|
|
560
565
|
role="button"
|
|
561
566
|
tabIndex={0}
|
|
562
|
-
aria-expanded={open}
|
|
563
567
|
onClick={onToggle}
|
|
564
568
|
onKeyDown={(e) => {
|
|
565
569
|
if (e.target !== e.currentTarget) return
|
|
@@ -585,18 +589,15 @@ export function CheckCardShell({
|
|
|
585
589
|
{SEVERITY_LABEL[sev]}
|
|
586
590
|
</span>
|
|
587
591
|
{renderActions?.()}
|
|
588
|
-
<
|
|
592
|
+
<CollapseChevron open={open} className="h-4 w-4" />
|
|
589
593
|
</div>
|
|
590
594
|
|
|
591
|
-
{/* Kept mounted (not `open &&`) so the
|
|
592
|
-
|
|
593
|
-
the clipped content. Body sits on the card surface (not a recessed grey
|
|
595
|
+
{/* Kept mounted (Collapse's default, not `open &&`) so the collapse
|
|
596
|
+
animates too. Body sits on the card surface (not a recessed grey
|
|
594
597
|
panel) so its text keeps enough contrast. */}
|
|
595
|
-
<
|
|
596
|
-
<div className="
|
|
597
|
-
|
|
598
|
-
</div>
|
|
599
|
-
</div>
|
|
598
|
+
<Collapse open={open} id={panelId}>
|
|
599
|
+
<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>
|
|
600
|
+
</Collapse>
|
|
600
601
|
</Container>
|
|
601
602
|
)
|
|
602
603
|
}
|
|
@@ -631,6 +632,9 @@ export function CheckClusterBreakdownShell<T extends CheckClusterBreakdownGroup>
|
|
|
631
632
|
const [showAllClusters, setShowAllClusters] = useState(false)
|
|
632
633
|
const shown = showAllClusters ? groups : groups.slice(0, clusterCap)
|
|
633
634
|
const hiddenClusters = groups.length - shown.length
|
|
635
|
+
// Group rows are mapped inline, so they can't each call useDisclosure; one
|
|
636
|
+
// generated prefix plus the group id keeps aria-controls unique.
|
|
637
|
+
const panelBase = useId()
|
|
634
638
|
|
|
635
639
|
return (
|
|
636
640
|
<section className="flex flex-col gap-1.5">
|
|
@@ -643,6 +647,7 @@ export function CheckClusterBreakdownShell<T extends CheckClusterBreakdownGroup>
|
|
|
643
647
|
<button
|
|
644
648
|
type="button"
|
|
645
649
|
aria-expanded={isOpen}
|
|
650
|
+
aria-controls={disclosurePanelId(panelBase, group.id)}
|
|
646
651
|
onClick={() =>
|
|
647
652
|
setOpenClusters((prev) => {
|
|
648
653
|
const next = new Set(prev)
|
|
@@ -653,7 +658,7 @@ export function CheckClusterBreakdownShell<T extends CheckClusterBreakdownGroup>
|
|
|
653
658
|
}
|
|
654
659
|
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors hover:bg-theme-hover/50"
|
|
655
660
|
>
|
|
656
|
-
<
|
|
661
|
+
<CollapseChevron open={isOpen} className="h-3.5 w-3.5" />
|
|
657
662
|
<span className="min-w-0 max-w-[260px] truncate text-sm font-medium text-theme-text-primary">{group.label}</span>
|
|
658
663
|
{group.environment && (
|
|
659
664
|
<span className="shrink-0 rounded bg-theme-elevated px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-theme-text-secondary ring-1 ring-theme-border">
|
|
@@ -663,11 +668,9 @@ export function CheckClusterBreakdownShell<T extends CheckClusterBreakdownGroup>
|
|
|
663
668
|
<span className="flex-1" />
|
|
664
669
|
<span className="shrink-0 text-xs tabular-nums text-theme-text-secondary">{group.count}</span>
|
|
665
670
|
</button>
|
|
666
|
-
<
|
|
667
|
-
<div className="
|
|
668
|
-
|
|
669
|
-
</div>
|
|
670
|
-
</div>
|
|
671
|
+
<Collapse open={isOpen} id={disclosurePanelId(panelBase, group.id)}>
|
|
672
|
+
<div className="px-2.5 pb-2 pl-7">{renderGroupBody(group)}</div>
|
|
673
|
+
</Collapse>
|
|
671
674
|
</li>
|
|
672
675
|
)
|
|
673
676
|
})}
|
|
@@ -906,6 +909,10 @@ function ClusterFilter({
|
|
|
906
909
|
onClear: () => void
|
|
907
910
|
}) {
|
|
908
911
|
const [menu, setMenu] = useState<{ top: number; left: number } | null>(null)
|
|
912
|
+
// `menu` is the logical open state; the placement outlives it so the exit
|
|
913
|
+
// transition plays where the menu was. Re-measured on the next open.
|
|
914
|
+
const placementRef = useRef<{ top: number; left: number } | null>(null)
|
|
915
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(menu != null, overlayExitMs('menu'))
|
|
909
916
|
const btnRef = useRef<HTMLButtonElement>(null)
|
|
910
917
|
useEffect(() => {
|
|
911
918
|
if (!menu) return
|
|
@@ -933,9 +940,13 @@ function ClusterFilter({
|
|
|
933
940
|
return
|
|
934
941
|
}
|
|
935
942
|
const r = btnRef.current?.getBoundingClientRect()
|
|
936
|
-
if (r)
|
|
943
|
+
if (r) {
|
|
944
|
+
placementRef.current = { top: r.bottom + 4, left: r.left }
|
|
945
|
+
setMenu(placementRef.current)
|
|
946
|
+
}
|
|
937
947
|
}
|
|
938
948
|
const active = selected.size > 0
|
|
949
|
+
const placement = menu ?? placementRef.current
|
|
939
950
|
|
|
940
951
|
return (
|
|
941
952
|
<>
|
|
@@ -953,14 +964,20 @@ function ClusterFilter({
|
|
|
953
964
|
Clusters
|
|
954
965
|
<ChevronDown className="h-3 w-3" />
|
|
955
966
|
</button>
|
|
956
|
-
{
|
|
967
|
+
{shouldRender && placement &&
|
|
957
968
|
createPortal(
|
|
958
969
|
<div
|
|
959
970
|
data-cluster-menu
|
|
960
971
|
role="listbox"
|
|
961
972
|
aria-multiselectable
|
|
962
|
-
|
|
963
|
-
|
|
973
|
+
inert={!menu}
|
|
974
|
+
className={[
|
|
975
|
+
'fixed z-[60] max-h-80 w-64 origin-top-left overflow-auto rounded-lg border border-theme-border bg-theme-surface py-1 shadow-xl',
|
|
976
|
+
TRANSITION_MENU,
|
|
977
|
+
isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
978
|
+
!menu && 'pointer-events-none',
|
|
979
|
+
].filter(Boolean).join(' ')}
|
|
980
|
+
style={{ top: placement.top, left: placement.left, ...overlayTransitionStyle(isOpen, 'menu') }}
|
|
964
981
|
>
|
|
965
982
|
{active && (
|
|
966
983
|
<button
|
|
@@ -1003,6 +1020,9 @@ function ClusterFilter({
|
|
|
1003
1020
|
// it; position captured at open time, any scroll/resize closes it.
|
|
1004
1021
|
function RowMenu({ items }: { items: { label: string; onClick: () => void }[] }) {
|
|
1005
1022
|
const [menu, setMenu] = useState<{ top: number; right: number } | null>(null)
|
|
1023
|
+
// See ClusterFilter: placement outlives the logical open state for the exit.
|
|
1024
|
+
const placementRef = useRef<{ top: number; right: number } | null>(null)
|
|
1025
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(menu != null, overlayExitMs('menu'))
|
|
1006
1026
|
const btnRef = useRef<HTMLButtonElement>(null)
|
|
1007
1027
|
|
|
1008
1028
|
useEffect(() => {
|
|
@@ -1029,8 +1049,12 @@ function RowMenu({ items }: { items: { label: string; onClick: () => void }[] })
|
|
|
1029
1049
|
return
|
|
1030
1050
|
}
|
|
1031
1051
|
const r = btnRef.current?.getBoundingClientRect()
|
|
1032
|
-
if (r)
|
|
1052
|
+
if (r) {
|
|
1053
|
+
placementRef.current = { top: r.bottom + 4, right: window.innerWidth - r.right }
|
|
1054
|
+
setMenu(placementRef.current)
|
|
1055
|
+
}
|
|
1033
1056
|
}
|
|
1057
|
+
const placement = menu ?? placementRef.current
|
|
1034
1058
|
|
|
1035
1059
|
return (
|
|
1036
1060
|
<>
|
|
@@ -1046,12 +1070,18 @@ function RowMenu({ items }: { items: { label: string; onClick: () => void }[] })
|
|
|
1046
1070
|
>
|
|
1047
1071
|
<MoreHorizontal className="h-4 w-4" />
|
|
1048
1072
|
</button>
|
|
1049
|
-
{
|
|
1073
|
+
{shouldRender && placement &&
|
|
1050
1074
|
createPortal(
|
|
1051
1075
|
<div
|
|
1052
1076
|
role="menu"
|
|
1053
|
-
|
|
1054
|
-
|
|
1077
|
+
inert={!menu}
|
|
1078
|
+
className={[
|
|
1079
|
+
'fixed z-[60] min-w-48 origin-top-right rounded-lg border border-theme-border bg-theme-surface py-1 shadow-xl',
|
|
1080
|
+
TRANSITION_MENU,
|
|
1081
|
+
isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
1082
|
+
!menu && 'pointer-events-none',
|
|
1083
|
+
].filter(Boolean).join(' ')}
|
|
1084
|
+
style={{ top: placement.top, right: placement.right, ...overlayTransitionStyle(isOpen, 'menu') }}
|
|
1055
1085
|
onClick={(e) => e.stopPropagation()}
|
|
1056
1086
|
>
|
|
1057
1087
|
{items.map((it, i) => (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, type ComponentType, type ReactNode } from 'react'
|
|
2
|
-
import { ArrowDownUp,
|
|
2
|
+
import { ArrowDownUp, Clock3, GitBranch, GitCommit, Loader2, Pause, Play, RefreshCw, Settings, Trash2, XCircle, Zap } from 'lucide-react'
|
|
3
|
+
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse'
|
|
3
4
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
4
5
|
|
|
5
6
|
import { HealthStatusBadge, SyncStatusBadge } from './GitOpsStatusBadge'
|
|
@@ -257,6 +258,10 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
257
258
|
documentTitleSuffix,
|
|
258
259
|
children,
|
|
259
260
|
} = props
|
|
261
|
+
// One predicate for header, caret and panel: without content there is
|
|
262
|
+
// nothing to open, so the header must not announce expanded.
|
|
263
|
+
const helmValuesShown = !!helmValuesOpen && !!helmValuesContent
|
|
264
|
+
const helmValuesDisclosure = useDisclosure(helmValuesShown)
|
|
260
265
|
|
|
261
266
|
// Document title side effect — opt-in so hub-web can take ownership of
|
|
262
267
|
// its own title format.
|
|
@@ -481,16 +486,12 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
481
486
|
{helmValues && onToggleHelmValues && (
|
|
482
487
|
<div className="shrink-0 border-b border-theme-border bg-theme-base">
|
|
483
488
|
<button
|
|
489
|
+
{...helmValuesDisclosure.buttonProps}
|
|
484
490
|
type="button"
|
|
485
491
|
onClick={onToggleHelmValues}
|
|
486
492
|
className="flex w-full items-center gap-2 px-4 py-2 text-left text-xs text-theme-text-secondary hover:bg-theme-hover"
|
|
487
|
-
aria-expanded={helmValuesOpen}
|
|
488
493
|
>
|
|
489
|
-
{
|
|
490
|
-
<ChevronDown className="h-3.5 w-3.5 shrink-0" />
|
|
491
|
-
) : (
|
|
492
|
-
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
|
|
493
|
-
)}
|
|
494
|
+
<CollapseChevron open={helmValuesShown} className="h-3.5 w-3.5" />
|
|
494
495
|
<Settings className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />
|
|
495
496
|
<span className="font-medium text-theme-text-primary">Helm values</span>
|
|
496
497
|
<span className="tabular-nums text-theme-text-tertiary">
|
|
@@ -500,11 +501,13 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
500
501
|
<span className="text-[10px] uppercase tracking-wider text-theme-text-tertiary">parameters</span>
|
|
501
502
|
)}
|
|
502
503
|
</button>
|
|
503
|
-
{
|
|
504
|
+
{/* The values viewer is a full code pane; keep it out of the tree
|
|
505
|
+
while closed rather than paying for it on every detail open. */}
|
|
506
|
+
<Collapse open={helmValuesShown} unmountOnExit id={helmValuesDisclosure.panelId}>
|
|
504
507
|
<div className="border-t border-theme-border bg-theme-surface px-4 py-3">
|
|
505
508
|
{helmValuesContent}
|
|
506
509
|
</div>
|
|
507
|
-
|
|
510
|
+
</Collapse>
|
|
508
511
|
</div>
|
|
509
512
|
)}
|
|
510
513
|
</>
|