@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.7",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/skyhook-io/radar",
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@types/node": "^26.2.0",
|
|
92
|
-
"@types/react": "^19.
|
|
93
|
-
"@types/react-dom": "^19.
|
|
92
|
+
"@types/react": "^19.3.0",
|
|
93
|
+
"@types/react-dom": "^19.3.0",
|
|
94
94
|
"@xterm/addon-fit": "^0.11.0",
|
|
95
95
|
"@xterm/addon-web-links": "^0.12.0",
|
|
96
96
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
"elkjs": "^0.11.1",
|
|
101
101
|
"lucide-react": "^1.37.0",
|
|
102
102
|
"monaco-editor": "0.55.1",
|
|
103
|
-
"react": "^19.
|
|
104
|
-
"react-dom": "^19.
|
|
103
|
+
"react": "^19.3.0",
|
|
104
|
+
"react-dom": "^19.3.0",
|
|
105
105
|
"typescript": "^6.0.2",
|
|
106
106
|
"vitest": "^4.1.11",
|
|
107
107
|
"yaml": "^2.9.0"
|
|
@@ -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,9 +1,10 @@
|
|
|
1
|
-
import { ClipboardCheck, ArrowRight, Check } from 'lucide-react'
|
|
1
|
+
import { ClipboardCheck, ArrowRight, Check, AlertTriangle } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { SEVERITY_TEXT, SEVERITY_DOT } from '../../utils/badge-colors'
|
|
4
4
|
import { SEVERITY_FILL_CLASS, SEVERITY_TEXT_CLASS } from '../checks/severity'
|
|
5
5
|
|
|
6
6
|
export interface AuditCardData {
|
|
7
|
+
missingInputs?: string[]
|
|
7
8
|
passing: number
|
|
8
9
|
/** Raw compatibility counts; warning renders as Medium and danger as High. */
|
|
9
10
|
warning: number
|
|
@@ -17,16 +18,17 @@ interface AuditCardProps {
|
|
|
17
18
|
onNavigate: () => void
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
type SeverityLevel = 'success' | 'high' | 'medium'
|
|
21
|
+
type SeverityLevel = 'success' | 'high' | 'medium' | 'neutral'
|
|
21
22
|
|
|
22
23
|
function getSeverityLevel(data: AuditCardData): SeverityLevel {
|
|
23
|
-
if (data.warning + data.danger === 0) return 'success'
|
|
24
|
+
if (data.warning + data.danger === 0) return data.missingInputs?.length || data.passing === 0 ? 'neutral' : 'success'
|
|
24
25
|
const highRatio = data.danger / (data.warning + data.danger)
|
|
25
26
|
if (highRatio > 0.2) return 'high'
|
|
26
27
|
return 'medium'
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const ACCENT_BG: Record<SeverityLevel, string> = {
|
|
31
|
+
neutral: 'bg-theme-elevated',
|
|
30
32
|
success: 'bg-green-500/10',
|
|
31
33
|
high: 'bg-orange-500/10',
|
|
32
34
|
medium: 'bg-yellow-500/10',
|
|
@@ -35,9 +37,10 @@ const ACCENT_BG: Record<SeverityLevel, string> = {
|
|
|
35
37
|
export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
36
38
|
const total = data.passing + data.warning + data.danger
|
|
37
39
|
const issueCount = data.warning + data.danger
|
|
38
|
-
const
|
|
40
|
+
const incomplete = (data.missingInputs?.length ?? 0) > 0
|
|
41
|
+
const allPassing = issueCount === 0 && total > 0 && !incomplete
|
|
39
42
|
const level = getSeverityLevel(data)
|
|
40
|
-
const accentColor = level === 'success' ? SEVERITY_TEXT.success : SEVERITY_TEXT_CLASS[level]
|
|
43
|
+
const accentColor = level === 'neutral' ? 'text-theme-text-secondary' : level === 'success' ? SEVERITY_TEXT.success : SEVERITY_TEXT_CLASS[level]
|
|
41
44
|
const accentBg = ACCENT_BG[level]
|
|
42
45
|
|
|
43
46
|
return (
|
|
@@ -56,7 +59,7 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
|
56
59
|
</span>
|
|
57
60
|
) : (
|
|
58
61
|
<span className={clsx('badge-sm', accentBg, accentColor)}>
|
|
59
|
-
<Check className="w-3 h-3" />
|
|
62
|
+
{allPassing ? <Check className="w-3 h-3" /> : <ClipboardCheck className="w-3 h-3" />}
|
|
60
63
|
</span>
|
|
61
64
|
)}
|
|
62
65
|
</div>
|
|
@@ -71,6 +74,12 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
|
71
74
|
<span className="text-xs text-theme-text-tertiary">{total} checks across {Object.keys(data.categories).length} categories</span>
|
|
72
75
|
)}
|
|
73
76
|
</div>
|
|
77
|
+
) : issueCount === 0 ? (
|
|
78
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
79
|
+
{incomplete ? <AlertTriangle className="w-8 h-8 text-warning-text" /> : <ClipboardCheck className="w-8 h-8 text-theme-text-tertiary" />}
|
|
80
|
+
<span className="text-sm font-medium text-theme-text-primary">{incomplete ? 'Some checks could not run' : 'No resources evaluated'}</span>
|
|
81
|
+
<span className="text-xs text-theme-text-secondary">{incomplete ? 'No findings in the available data.' : 'Check the selected scope and check settings.'}</span>
|
|
82
|
+
</div>
|
|
74
83
|
) : (
|
|
75
84
|
<>
|
|
76
85
|
{/* Distribution bar — only show when we have passing data for context */}
|
|
@@ -107,7 +116,7 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
|
107
116
|
)}
|
|
108
117
|
</div>
|
|
109
118
|
) : (
|
|
110
|
-
<span className={clsx('text-xs font-semibold', SEVERITY_TEXT.success)}>All passing</span>
|
|
119
|
+
<span className={clsx('text-xs font-semibold', incomplete ? 'text-theme-text-secondary' : SEVERITY_TEXT.success)}>{incomplete ? 'No findings' : 'All passing'}</span>
|
|
111
120
|
)}
|
|
112
121
|
</div>
|
|
113
122
|
)
|
|
@@ -117,6 +126,8 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
|
117
126
|
)}
|
|
118
127
|
</div>
|
|
119
128
|
|
|
129
|
+
{incomplete && issueCount > 0 && <p className="px-4 pb-2 text-xs text-warning-text">Some checks could not run.</p>}
|
|
130
|
+
|
|
120
131
|
<div className="px-4 py-1.5 border-t border-theme-border/50 flex items-center justify-end">
|
|
121
132
|
<span className={clsx(
|
|
122
133
|
'flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider transition-colors',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { AuditCard } from './AuditCard'
|
|
4
|
+
import { ChecksView } from '../checks/ChecksView'
|
|
5
|
+
import { FilterLocationProvider } from '../../filter-state'
|
|
6
|
+
|
|
7
|
+
describe('audit coverage presentation', () => {
|
|
8
|
+
for (const [passing, missingInputs, expected] of [
|
|
9
|
+
[0, [], 'No resources evaluated'],
|
|
10
|
+
[0, ['secrets'], 'Some checks could not run'],
|
|
11
|
+
[4, ['secrets'], 'Some checks could not run'],
|
|
12
|
+
[4, [], 'All checks passing'],
|
|
13
|
+
] as const) {
|
|
14
|
+
it(`shows ${expected} with ${passing} passing and ${missingInputs.length} unavailable inputs`, () => {
|
|
15
|
+
const html = renderToString(<AuditCard data={{ passing, missingInputs: [...missingInputs], warning: 0, danger: 0, categories: {} }} onNavigate={() => {}} />)
|
|
16
|
+
expect(html).toContain(expected)
|
|
17
|
+
if (missingInputs.length || passing === 0) expect(html).not.toContain('All checks passing')
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
it('keeps a coverage warning alongside real findings', () => {
|
|
21
|
+
const html = renderToString(<AuditCard data={{ passing: 1, missingInputs: ['secrets'], warning: 1, danger: 0, categories: { Security: { passing: 1, warning: 0, danger: 0 } } }} onNavigate={() => {}} />)
|
|
22
|
+
expect(html).toContain('Some checks could not run')
|
|
23
|
+
expect(html).not.toContain('All passing')
|
|
24
|
+
})
|
|
25
|
+
for (const [evaluated, missingInputs, expected] of [
|
|
26
|
+
[0, [], 'No resources evaluated'],
|
|
27
|
+
[0, ['secrets'], 'No findings in the available data'],
|
|
28
|
+
[4, ['secrets'], 'No findings in the available data'],
|
|
29
|
+
[4, [], 'Every audited resource passed'],
|
|
30
|
+
] as const) {
|
|
31
|
+
it(`Checks: ${expected}`, () => {
|
|
32
|
+
const html = renderToString(
|
|
33
|
+
<FilterLocationProvider value={{ searchParams: new URLSearchParams(), update: () => {} }}>
|
|
34
|
+
<ChecksView checks={[]} catalog={{}} anyData evaluated={evaluated} missingInputs={[...missingInputs]} />
|
|
35
|
+
</FilterLocationProvider>,
|
|
36
|
+
)
|
|
37
|
+
expect(html).toContain(expected)
|
|
38
|
+
if (missingInputs.length) {
|
|
39
|
+
expect(html).toContain('Unavailable inputs:')
|
|
40
|
+
expect(html).toContain('secrets')
|
|
41
|
+
expect(html).not.toContain('Every audited resource passed')
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
})
|
|
@@ -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
|
}
|
|
@@ -102,6 +102,19 @@ describe('AreaChart domain and axes', () => {
|
|
|
102
102
|
expect(attrs(render({ series: [series([[0, 1], [3600, 5]])] }), 'path', 'fill').filter(fill => fill === 'none')).toHaveLength(1)
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
+
it('breaks omitted evaluations only when the caller supplies the interval', () => {
|
|
106
|
+
const sparse = [series([[0, 1], [15, 2], [105, 3], [120, 4]])]
|
|
107
|
+
const paths = (stepSeconds?: number) => attrs(render({ series: sparse, stepSeconds }), 'path', 'fill').filter(fill => fill === 'none')
|
|
108
|
+
expect(paths(15)).toHaveLength(2)
|
|
109
|
+
expect(paths()).toHaveLength(1)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('preserves explicit gaps, zeros and isolated-sample behavior with a known interval', () => {
|
|
113
|
+
const gaps = [series([[0, 0], [15, 0], [30, null], [45, 1], [60, 2]])]
|
|
114
|
+
expect(attrs(render({ series: gaps, stepSeconds: 15 }), 'path', 'fill').filter(fill => fill === 'none')).toHaveLength(2)
|
|
115
|
+
expect(attrs(render({ series: [series([[0, 1]])], stepSeconds: 15 }), 'path', 'fill')).toHaveLength(0)
|
|
116
|
+
})
|
|
117
|
+
|
|
105
118
|
it('renders nothing for an empty series list', () => {
|
|
106
119
|
expect(render({ series: [] })).toBe('')
|
|
107
120
|
})
|