@skyhook-io/k8s-ui 1.14.5 → 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 +5 -5
- 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/AuditCard.tsx +18 -7
- package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
- package/src/components/audit/AuditFindingsTable.tsx +23 -27
- package/src/components/charts/AreaChart.test.tsx +13 -0
- package/src/components/charts/AreaChart.tsx +28 -25
- package/src/components/charts/PrometheusChartsView.tsx +41 -10
- package/src/components/charts/axis.test.ts +16 -0
- package/src/components/charts/axis.ts +3 -1
- package/src/components/charts/format.ts +4 -0
- package/src/components/charts/nearestSample.test.ts +31 -0
- package/src/components/charts/nearestSample.ts +15 -0
- package/src/components/checks/ChecksView.tsx +76 -28
- package/src/components/gitops/GitOpsDetailLayout.tsx +25 -9
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
- package/src/components/gitops/health-provenance.test.ts +54 -0
- package/src/components/gitops/health-provenance.ts +24 -0
- package/src/components/gitops/index.ts +2 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
- package/src/components/gitops/tree/merge.test.ts +34 -0
- package/src/components/gitops/tree/merge.ts +7 -0
- 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/ResourcesView.tsx +67 -2
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
- 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/ConfigMapRenderer.tsx +8 -1
- package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
- 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/LimitRangeRenderer.test.tsx +156 -0
- package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
- package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
- package/src/components/resources/renderers/PodRenderer.tsx +11 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +7 -4
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +13 -15
- package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
- package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
- package/src/components/resources/renderers/SbomReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/WorkloadRenderer.tsx +11 -1
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +123 -0
- package/src/components/resources/resource-utils.ts +18 -9
- package/src/components/shared/CreateResourceDialog.tsx +237 -14
- package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
- package/src/components/shared/DrainPlanDialog.tsx +297 -0
- package/src/components/shared/ResourceActionsBar.tsx +53 -26
- package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
- package/src/components/shared/index.ts +4 -0
- 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/FetchResult.test.tsx +20 -2
- package/src/components/ui/FetchResult.tsx +33 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +7 -4
- package/src/components/ui/FreshnessControl.tsx +8 -2
- 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 +8 -12
- package/src/hooks/useAnimatedUnmount.ts +21 -3
- package/src/theme/components.css +20 -1
- package/src/types/core.ts +15 -1
- package/src/types/gitops-insights.ts +23 -0
- package/src/types/gitops-tree.ts +29 -0
- package/src/utils/animation.ts +93 -13
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +20 -1
- package/src/utils/navigation.ts +25 -0
- package/src/utils/network-policy.test.ts +42 -0
- package/src/utils/network-policy.ts +59 -0
- package/src/utils/reflector.ts +139 -0
- package/src/utils/yaml-file-import.test.ts +189 -0
- package/src/utils/yaml-file-import.ts +95 -0
- package/src/components/logs/JsonLogLine.tsx +0 -93
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
CheckCircle,
|
|
8
8
|
Clock,
|
|
9
9
|
RefreshCw,
|
|
10
|
-
ChevronRight,
|
|
11
10
|
Plus,
|
|
12
11
|
Trash2,
|
|
13
12
|
Shield,
|
|
14
13
|
X,
|
|
15
14
|
} from 'lucide-react'
|
|
16
15
|
import { clsx } from 'clsx'
|
|
16
|
+
import { Collapse, CollapseChevron } from '../ui/Collapse'
|
|
17
17
|
import { DiffViewer, DiffBadge } from './DiffViewer'
|
|
18
18
|
import { TimelineToolbar } from './TimelineToolbar'
|
|
19
19
|
import {
|
|
@@ -696,17 +696,20 @@ function ActivityCard({ item, expanded, onToggle, onResourceClick, compact, sele
|
|
|
696
696
|
aria-label={expanded ? 'Collapse changes' : 'Expand changes'}
|
|
697
697
|
className="shrink-0 rounded p-0.5 text-theme-text-disabled hover:bg-theme-elevated hover:text-theme-text-secondary"
|
|
698
698
|
>
|
|
699
|
-
<
|
|
699
|
+
<CollapseChevron open={expanded} inheritColor className="w-4 h-4" />
|
|
700
700
|
</button>
|
|
701
701
|
)}
|
|
702
702
|
</div>
|
|
703
703
|
|
|
704
|
-
{/* Expanded details - only for items with diffs
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
<
|
|
709
|
-
|
|
704
|
+
{/* Expanded details - only for items with diffs. A list holds many
|
|
705
|
+
cards, each diff is real work to render: mount on first open. */}
|
|
706
|
+
{hasExpandableContent && item.diff && (
|
|
707
|
+
<Collapse open={expanded} mountLazily>
|
|
708
|
+
<div className="mt-3 pt-3 border-t-subtle">
|
|
709
|
+
<div className="text-xs text-theme-text-tertiary mb-2">Changes:</div>
|
|
710
|
+
<DiffViewer diff={item.diff} />
|
|
711
|
+
</div>
|
|
712
|
+
</Collapse>
|
|
710
713
|
)}
|
|
711
714
|
</div>
|
|
712
715
|
</div>
|
|
@@ -826,12 +829,12 @@ function AggregatedActivityCard({ first, last, count, reason, expanded, onToggle
|
|
|
826
829
|
aria-label={expanded ? 'Collapse occurrences' : 'Expand occurrences'}
|
|
827
830
|
className="shrink-0 rounded p-0.5 text-theme-text-disabled hover:bg-theme-elevated hover:text-theme-text-secondary"
|
|
828
831
|
>
|
|
829
|
-
<
|
|
832
|
+
<CollapseChevron open={expanded} inheritColor className="w-4 h-4" />
|
|
830
833
|
</button>
|
|
831
834
|
</div>
|
|
832
835
|
|
|
833
836
|
{/* Expanded details */}
|
|
834
|
-
{expanded
|
|
837
|
+
<Collapse open={expanded}>
|
|
835
838
|
<div className="mt-3 pt-3 border-t-subtle space-y-3">
|
|
836
839
|
{/* First occurrence */}
|
|
837
840
|
<div className="flex items-start gap-2">
|
|
@@ -865,7 +868,7 @@ function AggregatedActivityCard({ first, last, count, reason, expanded, onToggle
|
|
|
865
868
|
</div>
|
|
866
869
|
</div>
|
|
867
870
|
</div>
|
|
868
|
-
|
|
871
|
+
</Collapse>
|
|
869
872
|
</div>
|
|
870
873
|
</div>
|
|
871
874
|
)
|
|
@@ -26,6 +26,8 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
|
|
|
26
26
|
import { clsx } from 'clsx'
|
|
27
27
|
import { Calendar, ChevronDown, ChevronLeft, ChevronRight, Minus, Plus } from 'lucide-react'
|
|
28
28
|
import { Tooltip } from '../ui/Tooltip'
|
|
29
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
30
|
+
import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
29
31
|
import {
|
|
30
32
|
barHeight,
|
|
31
33
|
clampLensToSelection,
|
|
@@ -100,6 +102,7 @@ function DateTimeField({ label, valueMs, onChange, nowMs }: {
|
|
|
100
102
|
nowMs: number
|
|
101
103
|
}) {
|
|
102
104
|
const [open, setOpen] = useState(false)
|
|
105
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(open, overlayExitMs('menu'))
|
|
103
106
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
104
107
|
const value = new Date(valueMs)
|
|
105
108
|
const [viewY, setViewY] = useState(() => value.getFullYear())
|
|
@@ -145,9 +148,16 @@ function DateTimeField({ label, valueMs, onChange, nowMs }: {
|
|
|
145
148
|
{fieldStamp(valueMs)}
|
|
146
149
|
<Calendar className="h-3 w-3 shrink-0 text-theme-text-tertiary" />
|
|
147
150
|
</button>
|
|
148
|
-
{
|
|
151
|
+
{shouldRender && (
|
|
149
152
|
<div
|
|
150
|
-
|
|
153
|
+
inert={!open}
|
|
154
|
+
className={clsx(
|
|
155
|
+
'absolute left-0 top-full z-[60] mt-1 w-[232px] origin-top-left rounded-lg border border-theme-border bg-theme-surface p-2.5 shadow-theme-lg',
|
|
156
|
+
TRANSITION_MENU,
|
|
157
|
+
isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
158
|
+
!open && 'pointer-events-none',
|
|
159
|
+
)}
|
|
160
|
+
style={overlayTransitionStyle(isOpen, 'menu')}
|
|
151
161
|
role="dialog"
|
|
152
162
|
aria-label={`Pick ${label.toLowerCase()} date and time`}
|
|
153
163
|
>
|
|
@@ -294,6 +304,7 @@ export function TimelineStrip({
|
|
|
294
304
|
const trackRef = useRef<HTMLDivElement>(null)
|
|
295
305
|
const [width, setWidth] = useState(800)
|
|
296
306
|
const [pickerOpen, setPickerOpen] = useState(false)
|
|
307
|
+
const { shouldRender: pickerRender, isOpen: pickerShown } = useAnimatedUnmount(pickerOpen, overlayExitMs('menu'))
|
|
297
308
|
const [customFromMs, setCustomFromMs] = useState(0)
|
|
298
309
|
const [customToMs, setCustomToMs] = useState(0)
|
|
299
310
|
|
|
@@ -528,11 +539,22 @@ export function TimelineStrip({
|
|
|
528
539
|
<ChevronDown className="h-3 w-3 text-theme-text-tertiary" />
|
|
529
540
|
</button>
|
|
530
541
|
</Tooltip>
|
|
531
|
-
{
|
|
542
|
+
{pickerRender && (
|
|
532
543
|
// NO overflow-hidden: the DateTimeField calendars pop past the
|
|
533
544
|
// dialog's bottom edge and were getting clipped to a header sliver.
|
|
534
545
|
// The footer rounds its own bottom corners instead.
|
|
535
|
-
<div
|
|
546
|
+
<div
|
|
547
|
+
inert={!pickerOpen}
|
|
548
|
+
className={clsx(
|
|
549
|
+
'absolute left-0 top-full z-50 mt-2 w-[460px] max-w-[92vw] origin-top-left rounded-xl border border-theme-border bg-theme-surface shadow-theme-lg',
|
|
550
|
+
TRANSITION_MENU,
|
|
551
|
+
pickerShown ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
552
|
+
!pickerOpen && 'pointer-events-none',
|
|
553
|
+
)}
|
|
554
|
+
style={overlayTransitionStyle(pickerShown, 'menu')}
|
|
555
|
+
role="dialog"
|
|
556
|
+
aria-label="Time range picker"
|
|
557
|
+
>
|
|
536
558
|
<p className="border-b border-theme-border/60 px-4 py-2.5 text-[11px] leading-relaxed text-theme-text-secondary">
|
|
537
559
|
How much history to load. The blue band zooms into a slice of it.
|
|
538
560
|
</p>
|
|
@@ -43,6 +43,7 @@ import { isChangeEvent, isHistoricalEvent, isOperation, displayKind } from '../.
|
|
|
43
43
|
import { DiffViewer } from './DiffViewer'
|
|
44
44
|
import { getHealthBadgeColor, getEventTypeColor } from '../../utils/badge-colors'
|
|
45
45
|
import { MiddleEllipsis } from '../ui/MiddleEllipsis'
|
|
46
|
+
import { CollapseChevron } from '../ui/Collapse'
|
|
46
47
|
import { Tooltip } from '../ui/Tooltip'
|
|
47
48
|
import { ResourceRefBadge } from '../ui/drawer-components'
|
|
48
49
|
import { buildResourceHierarchy, extractPinnedLanes, removePinnedLanes, isProblematicEvent, laneTrackEvents, isChildVisibleInWindow, collidingLaneKeys, laneCollisionKey, type ResourceLane as BaseResourceLane, type TimelineGrouping, type PinnedLaneRef } from '../../utils/resource-hierarchy'
|
|
@@ -1644,10 +1645,7 @@ export function TimelineSwimlanes({ events, isLoading, onResourceClick, viewMode
|
|
|
1644
1645
|
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
|
1645
1646
|
className="p-1 -m-0.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
1646
1647
|
>
|
|
1647
|
-
<
|
|
1648
|
-
'w-4 h-4 transition-transform',
|
|
1649
|
-
isExpanded && 'rotate-90'
|
|
1650
|
-
)} />
|
|
1648
|
+
<CollapseChevron open={isExpanded} inheritColor className="w-4 h-4" />
|
|
1651
1649
|
</button>
|
|
1652
1650
|
) : compact ? null : (
|
|
1653
1651
|
<div className="w-4" />
|
|
@@ -2285,7 +2283,7 @@ function ChildLaneLabel({ kind, group, showGroupChip, kindTitle, name, labelWidt
|
|
|
2285
2283
|
style={{ left: selfRailPx }}
|
|
2286
2284
|
aria-label={expanded ? 'Collapse' : 'Expand'}
|
|
2287
2285
|
>
|
|
2288
|
-
<
|
|
2286
|
+
<CollapseChevron open={!!expanded} inheritColor className="w-4 h-4" />
|
|
2289
2287
|
</button>
|
|
2290
2288
|
)}
|
|
2291
2289
|
{/* Single-line child row: kind chip · name (middle-ellipsis). Colored chips
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
import { SearchBox } from '../ui/SearchBox'
|
|
19
19
|
import { type ShortcutScope } from '../../hooks/useKeyboardShortcuts'
|
|
20
20
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
21
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
22
|
+
import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
21
23
|
import { pluralize } from '../../utils/pluralize'
|
|
22
24
|
import type { TimelineEvent, TimeRange } from '../../types'
|
|
23
25
|
import type { TimelineGrouping } from '../../utils/resource-hierarchy'
|
|
@@ -475,6 +477,7 @@ function SegmentedRadioGroup<T extends string>({
|
|
|
475
477
|
*/
|
|
476
478
|
export function ViewMenu({ viewOptions }: { viewOptions: TimelineViewOptions }) {
|
|
477
479
|
const [open, setOpen] = useState(false)
|
|
480
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(open, overlayExitMs('menu'))
|
|
478
481
|
const rootRef = useRef<HTMLDivElement | null>(null)
|
|
479
482
|
usePopoverDismiss(open, setOpen, rootRef)
|
|
480
483
|
|
|
@@ -497,11 +500,18 @@ export function ViewMenu({ viewOptions }: { viewOptions: TimelineViewOptions })
|
|
|
497
500
|
<ChevronDown className="w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
498
501
|
</button>
|
|
499
502
|
|
|
500
|
-
{
|
|
503
|
+
{shouldRender && (
|
|
501
504
|
<div
|
|
502
505
|
role="menu"
|
|
503
506
|
aria-label="View options"
|
|
504
|
-
|
|
507
|
+
inert={!open}
|
|
508
|
+
className={clsx(
|
|
509
|
+
'absolute right-0 top-full z-50 mt-1 min-w-[14rem] origin-top-right rounded-lg border border-theme-border bg-theme-elevated p-2 shadow-theme-lg',
|
|
510
|
+
TRANSITION_MENU,
|
|
511
|
+
isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
512
|
+
!open && 'pointer-events-none',
|
|
513
|
+
)}
|
|
514
|
+
style={overlayTransitionStyle(isOpen, 'menu')}
|
|
505
515
|
>
|
|
506
516
|
<span className="block px-2 pb-1 text-[10px] font-bold uppercase tracking-[0.08em] text-theme-text-tertiary">Sort</span>
|
|
507
517
|
<SegmentedRadioGroup
|
|
@@ -634,10 +644,11 @@ function usePopoverDismiss(
|
|
|
634
644
|
*/
|
|
635
645
|
function KindsMenu({ kindFilter, onKindFilterChange, kindOptions }: KindsMenuProps) {
|
|
636
646
|
const [open, setOpen] = useState(false)
|
|
647
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(open, overlayExitMs('menu'))
|
|
637
648
|
const [search, setSearch] = useState('')
|
|
638
649
|
const rootRef = useRef<HTMLDivElement | null>(null)
|
|
639
650
|
const panelRef = useRef<HTMLDivElement | null>(null)
|
|
640
|
-
const [anchor, setAnchor] = useState<{ left: number; top: number; maxHeight: number } | null>(null)
|
|
651
|
+
const [anchor, setAnchor] = useState<{ left: number; top: number; maxHeight: number; flipUp: boolean } | null>(null)
|
|
641
652
|
const activeCount = kindFilter.length
|
|
642
653
|
const selected = useMemo(() => new Set(kindFilter), [kindFilter])
|
|
643
654
|
|
|
@@ -645,9 +656,11 @@ function KindsMenu({ kindFilter, onKindFilterChange, kindOptions }: KindsMenuPro
|
|
|
645
656
|
// clips absolutely-positioned children. Portal the popover to the body and pin
|
|
646
657
|
// it to the button with fixed coords so it escapes the clip. Dismiss must treat
|
|
647
658
|
// BOTH the chip and the portaled panel as "inside".
|
|
659
|
+
// The anchor is kept through the exit transition (clearing it at logical
|
|
660
|
+
// close would unmount the panel mid-fade) and re-placed on the next open.
|
|
648
661
|
const PANEL_WIDTH = 256
|
|
649
662
|
useLayoutEffect(() => {
|
|
650
|
-
if (!open)
|
|
663
|
+
if (!open) return
|
|
651
664
|
const place = () => {
|
|
652
665
|
const r = rootRef.current?.getBoundingClientRect()
|
|
653
666
|
if (!r) return
|
|
@@ -666,11 +679,13 @@ function KindsMenu({ kindFilter, onKindFilterChange, kindOptions }: KindsMenuPro
|
|
|
666
679
|
const top = flipUp
|
|
667
680
|
? Math.max(4, r.top - 4 - Math.min(panelH, maxHeight))
|
|
668
681
|
: r.bottom + 4
|
|
669
|
-
setAnchor({ left, top, maxHeight })
|
|
682
|
+
setAnchor({ left, top, maxHeight, flipUp })
|
|
670
683
|
}
|
|
671
684
|
place()
|
|
672
685
|
// Re-place after the panel mounts so the first open can measure its height and
|
|
673
|
-
// flip; the initial pass runs with panelH=0 (panel not yet in the DOM).
|
|
686
|
+
// flip; the initial pass runs with panelH=0 (panel not yet in the DOM). The
|
|
687
|
+
// presence hook mounts the panel a tick after `open`, hence `shouldRender` in
|
|
688
|
+
// the deps: the pass that runs once it is in the DOM does the real measurement.
|
|
674
689
|
const raf = requestAnimationFrame(place)
|
|
675
690
|
window.addEventListener('scroll', place, true)
|
|
676
691
|
window.addEventListener('resize', place)
|
|
@@ -679,7 +694,7 @@ function KindsMenu({ kindFilter, onKindFilterChange, kindOptions }: KindsMenuPro
|
|
|
679
694
|
window.removeEventListener('scroll', place, true)
|
|
680
695
|
window.removeEventListener('resize', place)
|
|
681
696
|
}
|
|
682
|
-
}, [open])
|
|
697
|
+
}, [open, shouldRender])
|
|
683
698
|
|
|
684
699
|
useEffect(() => {
|
|
685
700
|
if (!open) return
|
|
@@ -731,11 +746,29 @@ function KindsMenu({ kindFilter, onKindFilterChange, kindOptions }: KindsMenuPro
|
|
|
731
746
|
<ChevronDown className="w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
732
747
|
</button>
|
|
733
748
|
|
|
734
|
-
{
|
|
749
|
+
{shouldRender && anchor && createPortal(
|
|
735
750
|
<div
|
|
736
751
|
ref={panelRef}
|
|
737
|
-
|
|
738
|
-
|
|
752
|
+
inert={!open}
|
|
753
|
+
style={{
|
|
754
|
+
position: 'fixed',
|
|
755
|
+
left: anchor.left,
|
|
756
|
+
top: anchor.top,
|
|
757
|
+
width: PANEL_WIDTH,
|
|
758
|
+
maxHeight: anchor.maxHeight,
|
|
759
|
+
...overlayTransitionStyle(isOpen, 'menu'),
|
|
760
|
+
}}
|
|
761
|
+
className={clsx(
|
|
762
|
+
'z-50 overflow-y-auto rounded-md border border-theme-border bg-theme-surface shadow-theme-lg',
|
|
763
|
+
anchor.flipUp ? 'origin-bottom-left' : 'origin-top-left',
|
|
764
|
+
TRANSITION_MENU,
|
|
765
|
+
isOpen
|
|
766
|
+
? 'opacity-100 translate-y-0 scale-100'
|
|
767
|
+
: anchor.flipUp
|
|
768
|
+
? 'opacity-0 translate-y-1 scale-[0.97]'
|
|
769
|
+
: 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
770
|
+
!open && 'pointer-events-none',
|
|
771
|
+
)}
|
|
739
772
|
>
|
|
740
773
|
<MultiSelectPicker
|
|
741
774
|
items={kindOptions}
|
|
@@ -3,6 +3,8 @@ import { FolderTree, ShieldCheck, ChevronDown, Check } from 'lucide-react'
|
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import type { TopologyMode, GroupingMode } from '../../types/core'
|
|
5
5
|
import { Tooltip } from '../ui/Tooltip'
|
|
6
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
7
|
+
import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
6
8
|
|
|
7
9
|
interface TopologyControlsProps {
|
|
8
10
|
viewMode: TopologyMode
|
|
@@ -37,6 +39,7 @@ export function TopologyControls({
|
|
|
37
39
|
leadingSlot,
|
|
38
40
|
}: TopologyControlsProps) {
|
|
39
41
|
const [groupOpen, setGroupOpen] = useState(false)
|
|
42
|
+
const { shouldRender: groupRender, isOpen: groupShown } = useAnimatedUnmount(groupOpen, overlayExitMs('menu'))
|
|
40
43
|
const groupRef = useRef<HTMLDivElement>(null)
|
|
41
44
|
const groupTriggerRef = useRef<HTMLButtonElement>(null)
|
|
42
45
|
const groupItemRefs = useRef<(HTMLButtonElement | null)[]>([])
|
|
@@ -53,20 +56,25 @@ export function TopologyControls({
|
|
|
53
56
|
if (restoreFocus) groupTriggerRef.current?.focus()
|
|
54
57
|
}, [])
|
|
55
58
|
|
|
56
|
-
// On open, move focus onto the active option so the menu is keyboard-navigable
|
|
57
|
-
//
|
|
59
|
+
// On open, move focus onto the active option so the menu is keyboard-navigable.
|
|
60
|
+
// The items exist only once the presence hook has mounted the menu, so this
|
|
61
|
+
// waits for `groupRender`.
|
|
58
62
|
useEffect(() => {
|
|
59
|
-
if (!groupOpen) return
|
|
63
|
+
if (!groupOpen || !groupRender) return
|
|
60
64
|
const active = Math.max(0, groupOptions.findIndex((o) => o.value === groupingMode))
|
|
61
65
|
groupItemRefs.current[active]?.focus()
|
|
66
|
+
// groupOptions/groupingMode are read once at open; re-running on their
|
|
67
|
+
// identity change would steal focus mid-interaction.
|
|
68
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
+
}, [groupOpen, groupRender])
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!groupOpen) return
|
|
62
73
|
const onDown = (e: MouseEvent) => {
|
|
63
74
|
if (groupRef.current && !groupRef.current.contains(e.target as Node)) setGroupOpen(false)
|
|
64
75
|
}
|
|
65
76
|
document.addEventListener('mousedown', onDown)
|
|
66
77
|
return () => document.removeEventListener('mousedown', onDown)
|
|
67
|
-
// groupOptions/groupingMode are read once at open; re-running on their
|
|
68
|
-
// identity change would steal focus mid-interaction.
|
|
69
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
78
|
}, [groupOpen])
|
|
71
79
|
|
|
72
80
|
const onGroupMenuKey = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
@@ -123,8 +131,19 @@ export function TopologyControls({
|
|
|
123
131
|
{currentGroupLabel}
|
|
124
132
|
<ChevronDown className="w-3 h-3 text-theme-text-tertiary" />
|
|
125
133
|
</button>
|
|
126
|
-
{
|
|
127
|
-
<div
|
|
134
|
+
{groupRender && (
|
|
135
|
+
<div
|
|
136
|
+
role="menu"
|
|
137
|
+
onKeyDown={onGroupMenuKey}
|
|
138
|
+
inert={!groupOpen}
|
|
139
|
+
className={clsx(
|
|
140
|
+
'absolute right-0 top-full mt-1 z-50 min-w-[160px] origin-top-right rounded-lg border border-theme-border bg-theme-surface py-1 shadow-xl',
|
|
141
|
+
TRANSITION_MENU,
|
|
142
|
+
groupShown ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
143
|
+
!groupOpen && 'pointer-events-none',
|
|
144
|
+
)}
|
|
145
|
+
style={overlayTransitionStyle(groupShown, 'menu')}
|
|
146
|
+
>
|
|
128
147
|
{groupOptions.map((o, idx) => (
|
|
129
148
|
<button
|
|
130
149
|
key={o.value}
|
|
@@ -23,6 +23,7 @@ import { toCanvas } from 'html-to-image'
|
|
|
23
23
|
|
|
24
24
|
import { AlertTriangle, ChevronsDownUp, ChevronsUpDown, Download, Info, Layers, LayoutGrid, Loader2, Maximize, Minus, Pause, Play, Plus, RotateCw, Shield } from 'lucide-react'
|
|
25
25
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
26
|
+
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse'
|
|
26
27
|
import { TopologyOverlayBar } from './TopologyOverlayBar'
|
|
27
28
|
import { Input } from '../ui/Input'
|
|
28
29
|
import { Tooltip } from '../ui/Tooltip'
|
|
@@ -315,6 +316,8 @@ export function TopologyGraph({
|
|
|
315
316
|
const [fitViewCounter, setFitViewCounter] = useState(0)
|
|
316
317
|
const [isExporting, setIsExporting] = useState(false)
|
|
317
318
|
const [showLegend, setShowLegend] = useState(false)
|
|
319
|
+
const [warningsOpen, setWarningsOpen] = useState(false)
|
|
320
|
+
const warningsDisclosure = useDisclosure(warningsOpen)
|
|
318
321
|
const prevStructureRef = useRef<string>('')
|
|
319
322
|
const layoutVersionRef = useRef(0) // Used to invalidate stale layout results
|
|
320
323
|
// Saved node positions for preservation across topology updates.
|
|
@@ -1064,10 +1067,16 @@ export function TopologyGraph({
|
|
|
1064
1067
|
? 'Large retained run history is summarized to keep the graph readable.'
|
|
1065
1068
|
: 'Some resources failed to load. Data may be incomplete.'}
|
|
1066
1069
|
</span>
|
|
1067
|
-
<
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1070
|
+
<button
|
|
1071
|
+
{...warningsDisclosure.buttonProps}
|
|
1072
|
+
type="button"
|
|
1073
|
+
onClick={() => setWarningsOpen((v) => !v)}
|
|
1074
|
+
className="mt-1 flex items-center gap-1 text-xs text-amber-400/80 hover:text-amber-400"
|
|
1075
|
+
>
|
|
1076
|
+
<CollapseChevron open={warningsOpen} inheritColor className="h-3 w-3" />
|
|
1077
|
+
Show details ({topology.warnings.length})
|
|
1078
|
+
</button>
|
|
1079
|
+
<Collapse open={warningsOpen} id={warningsDisclosure.panelId}>
|
|
1071
1080
|
<ul className="mt-1 text-xs text-theme-text-tertiary space-y-0.5">
|
|
1072
1081
|
{rbacWarnings.length > 0 && otherWarnings.length > 0 && (
|
|
1073
1082
|
<li className="text-amber-400/60 font-medium mt-1">RBAC restrictions:</li>
|
|
@@ -1088,7 +1097,7 @@ export function TopologyGraph({
|
|
|
1088
1097
|
<li key={`other-${i}`} className="font-mono">{w}</li>
|
|
1089
1098
|
))}
|
|
1090
1099
|
</ul>
|
|
1091
|
-
</
|
|
1100
|
+
</Collapse>
|
|
1092
1101
|
</div>
|
|
1093
1102
|
</div>
|
|
1094
1103
|
</div>
|
|
@@ -4,7 +4,8 @@ import { ReachActions, JustTestedNote, CopyableCommand, completedRequestMode, ty
|
|
|
4
4
|
import { AlertBanner } from '../ui/drawer-components'
|
|
5
5
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
6
6
|
import { ReachabilityGraph, MarkGlyph } from './ReachabilityGraph'
|
|
7
|
-
import { Activity
|
|
7
|
+
import { Activity } from 'lucide-react'
|
|
8
|
+
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse'
|
|
8
9
|
import { Tooltip } from '../ui/Tooltip'
|
|
9
10
|
import { buildGraph, noteHeadline } from './reachGraphModel'
|
|
10
11
|
import { buildOrigins, defaultOrigin, probeCheckStats, type Origin, type OriginId } from './reachOrigins'
|
|
@@ -778,10 +779,10 @@ function untestableSummary(out: Origin[]): string {
|
|
|
778
779
|
// ---------------------------------------------------------------- inspector
|
|
779
780
|
|
|
780
781
|
/**
|
|
781
|
-
* The panel's one disclosure,
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
782
|
+
* The panel's one disclosure, on the shared Collapse primitive like
|
|
783
|
+
* drawer-components' Section. Hand-rolled "›"/"⌄" toggles neither looked like
|
|
784
|
+
* the rest of Radar nor animated, so a section appearing was an abrupt jump
|
|
785
|
+
* with no affordance saying it could be opened at all.
|
|
785
786
|
*/
|
|
786
787
|
function Disclosure({
|
|
787
788
|
open,
|
|
@@ -796,21 +797,22 @@ function Disclosure({
|
|
|
796
797
|
count?: number
|
|
797
798
|
children: React.ReactNode
|
|
798
799
|
}) {
|
|
800
|
+
const { panelId, buttonProps } = useDisclosure(open)
|
|
799
801
|
return (
|
|
800
802
|
<div>
|
|
801
803
|
<button
|
|
804
|
+
{...buttonProps}
|
|
802
805
|
type="button"
|
|
803
806
|
onClick={onToggle}
|
|
804
|
-
aria-expanded={open}
|
|
805
807
|
className="flex w-full items-center gap-1.5 text-[9.5px] font-bold tracking-[0.07em] text-theme-text-tertiary transition-colors hover:text-theme-text-secondary"
|
|
806
808
|
>
|
|
807
|
-
<
|
|
809
|
+
<CollapseChevron open={open} className="h-3 w-3 flex-none" />
|
|
808
810
|
<span>{label}</span>
|
|
809
811
|
{count !== undefined && <span className="flex-1 text-right font-normal tracking-normal">{count}</span>}
|
|
810
812
|
</button>
|
|
811
|
-
<
|
|
812
|
-
|
|
813
|
-
</
|
|
813
|
+
<Collapse open={open} id={panelId}>
|
|
814
|
+
{children}
|
|
815
|
+
</Collapse>
|
|
814
816
|
</div>
|
|
815
817
|
)
|
|
816
818
|
}
|
|
@@ -981,6 +983,7 @@ function HopSection({
|
|
|
981
983
|
}) {
|
|
982
984
|
const [openOverride, setOpenOverride] = useState<boolean | null>(null)
|
|
983
985
|
const open = openOverride ?? hop.expanded
|
|
986
|
+
const { panelId, buttonProps } = useDisclosure(open)
|
|
984
987
|
const ref = useRef<HTMLDivElement>(null)
|
|
985
988
|
// A click on the graph scrolls its hop into view instead of replacing the
|
|
986
989
|
// panel - the gesture still means "I care about this one", it just no longer
|
|
@@ -1012,15 +1015,15 @@ function HopSection({
|
|
|
1012
1015
|
className={`mt-2 rounded-md px-2 py-1 transition-colors duration-200 ${selected ? 'selection selection-ring' : ''}`}
|
|
1013
1016
|
>
|
|
1014
1017
|
<button
|
|
1018
|
+
{...buttonProps}
|
|
1015
1019
|
type="button"
|
|
1016
1020
|
onClick={() => {
|
|
1017
1021
|
setOpenOverride(!open)
|
|
1018
1022
|
onSelect(hop.id)
|
|
1019
1023
|
}}
|
|
1020
|
-
aria-expanded={open}
|
|
1021
1024
|
className="flex w-full items-center gap-1.5 text-left"
|
|
1022
1025
|
>
|
|
1023
|
-
<
|
|
1026
|
+
<CollapseChevron open={open} className="h-3 w-3 flex-none" />
|
|
1024
1027
|
<span className="text-[9px] font-bold tracking-[0.04em] text-theme-text-tertiary">{hop.kind}</span>
|
|
1025
1028
|
<span className="min-w-0 flex-1 truncate font-mono text-[11.5px] font-semibold text-theme-text-primary">{hop.name}</span>
|
|
1026
1029
|
<span className={`badge-sm whitespace-nowrap ${SEV_BADGE[hop.chipTone]}`}>{hop.chipText || HOP_STATE_NOTE[hop.state]}</span>
|
|
@@ -1032,8 +1035,7 @@ function HopSection({
|
|
|
1032
1035
|
.join(' — ')}
|
|
1033
1036
|
</div>
|
|
1034
1037
|
)}
|
|
1035
|
-
<
|
|
1036
|
-
<div className="overflow-hidden">
|
|
1038
|
+
<Collapse open={open} id={panelId}>
|
|
1037
1039
|
<div className="pb-1 pl-[18px]">
|
|
1038
1040
|
{hop.openRef?.name && (
|
|
1039
1041
|
<button type="button" onClick={() => onOpen(hop.openRef!)} className="text-[11px] text-accent-text hover:underline">
|
|
@@ -1084,8 +1086,7 @@ function HopSection({
|
|
|
1084
1086
|
<Caveats items={hop.notProve} />
|
|
1085
1087
|
</div>
|
|
1086
1088
|
</div>
|
|
1087
|
-
|
|
1088
|
-
</div>
|
|
1089
|
+
</Collapse>
|
|
1089
1090
|
</div>
|
|
1090
1091
|
)
|
|
1091
1092
|
}
|