@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
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AlertOctagon, AlertTriangle, ArrowRight,
|
|
1
|
+
import { useMemo, useState, type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
import { AlertOctagon, AlertTriangle, ArrowRight, CircleCheck, Clock, ExternalLink, Layers, Terminal, Workflow } from 'lucide-react';
|
|
3
3
|
import { CardBody, CardSection, ClusterName, EmptyState, KIND_CHIP_CLASS, TerminalBlock } from '../ui';
|
|
4
|
+
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse';
|
|
4
5
|
import { Tooltip } from '../ui/Tooltip';
|
|
5
6
|
import { formatCompactAge, formatRelativeAgeTime } from '../../utils/format';
|
|
6
7
|
import { diagnosticRoleLabel, diagnosticFactLabel, confidenceTitle, incidentParentLabel } from './diagnostic';
|
|
@@ -207,7 +208,7 @@ export function IssueRow({
|
|
|
207
208
|
const cluster = clusterLabel?.(issue);
|
|
208
209
|
const affected = affectedSummary(issue.affected);
|
|
209
210
|
const { headline } = issueMessageParts(issue);
|
|
210
|
-
const
|
|
211
|
+
const { panelId, buttonProps } = useDisclosure(open);
|
|
211
212
|
const Container = as;
|
|
212
213
|
const severity = normalizeIssueSeverity(issue.severity);
|
|
213
214
|
const SeverityIcon = ISSUE_SEVERITY_ICON[severity];
|
|
@@ -244,17 +245,6 @@ export function IssueRow({
|
|
|
244
245
|
</div>
|
|
245
246
|
);
|
|
246
247
|
|
|
247
|
-
useEffect(() => {
|
|
248
|
-
if (open) {
|
|
249
|
-
setRenderDetails(true);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
if (!renderDetails) return;
|
|
253
|
-
|
|
254
|
-
const timeout = window.setTimeout(() => setRenderDetails(false), 200);
|
|
255
|
-
return () => window.clearTimeout(timeout);
|
|
256
|
-
}, [open, renderDetails]);
|
|
257
|
-
|
|
258
248
|
return (
|
|
259
249
|
<Container
|
|
260
250
|
className={[
|
|
@@ -276,9 +266,9 @@ export function IssueRow({
|
|
|
276
266
|
be invalid). Collapsed: neutral row + rail. Expanded: severity-tinted
|
|
277
267
|
band + solid pill — the tint is a focus signal, not per-row alarm. */}
|
|
278
268
|
<div
|
|
269
|
+
{...buttonProps}
|
|
279
270
|
role="button"
|
|
280
271
|
tabIndex={0}
|
|
281
|
-
aria-expanded={open}
|
|
282
272
|
onClick={onToggle}
|
|
283
273
|
onKeyDown={(e) => {
|
|
284
274
|
if (e.target !== e.currentTarget) return;
|
|
@@ -357,50 +347,41 @@ export function IssueRow({
|
|
|
357
347
|
<div className="flex shrink-0 items-center gap-3">
|
|
358
348
|
{metaChips('hidden @2xl/issue:flex')}
|
|
359
349
|
{renderActions?.(slotCtx)}
|
|
360
|
-
<
|
|
350
|
+
<CollapseChevron open={open} className="h-4 w-4" />
|
|
361
351
|
</div>
|
|
362
352
|
</div>
|
|
363
353
|
|
|
364
|
-
{
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
<ResourceLine refForLink={memberRef(issue, issue.incident_parent.ref)} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
393
|
-
</ul>
|
|
394
|
-
</section>
|
|
395
|
-
) : null}
|
|
396
|
-
<DiagnosticContext issue={issue} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
397
|
-
<AffectedResources issue={issue} hideSubject={hideSubject} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
398
|
-
{renderDetailSection?.(slotCtx)}
|
|
399
|
-
</div>
|
|
400
|
-
</div>
|
|
354
|
+
{/* Details unmount after the close transition: a list can hold hundreds
|
|
355
|
+
of rows and the body is the expensive part, so closed rows stay light. */}
|
|
356
|
+
<Collapse open={open} unmountOnExit id={panelId}>
|
|
357
|
+
{/* Body sits on the card surface (not a recessed grey panel) so its
|
|
358
|
+
text keeps enough contrast. */}
|
|
359
|
+
<div className="border-t border-theme-border bg-theme-surface py-4 pl-6 pr-4">
|
|
360
|
+
<div className="flex flex-col divide-y divide-theme-border/70 [&>*]:py-4 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0">
|
|
361
|
+
<Diagnosis issue={issue} source={diagnosisSource} />
|
|
362
|
+
{issue.incident_parent ? (
|
|
363
|
+
<section className="flex flex-col gap-1">
|
|
364
|
+
<h4 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
365
|
+
{incidentParentLabel(issue.incident_parent.fact_type, issue.incident_parent.confidence)}
|
|
366
|
+
{issue.incident_parent.confidence ? (
|
|
367
|
+
<Tooltip content={confidenceTitle(issue.incident_parent.confidence)} delay={200}>
|
|
368
|
+
<span className="ml-2 badge-sm text-[10px] font-normal text-theme-text-tertiary">
|
|
369
|
+
{issue.incident_parent.confidence} confidence
|
|
370
|
+
</span>
|
|
371
|
+
</Tooltip>
|
|
372
|
+
) : null}
|
|
373
|
+
</h4>
|
|
374
|
+
<ul className="flex flex-col gap-px">
|
|
375
|
+
<ResourceLine refForLink={memberRef(issue, issue.incident_parent.ref)} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
376
|
+
</ul>
|
|
377
|
+
</section>
|
|
378
|
+
) : null}
|
|
379
|
+
<DiagnosticContext issue={issue} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
380
|
+
<AffectedResources issue={issue} hideSubject={hideSubject} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
381
|
+
{renderDetailSection?.(slotCtx)}
|
|
401
382
|
</div>
|
|
402
383
|
</div>
|
|
403
|
-
|
|
384
|
+
</Collapse>
|
|
404
385
|
</Container>
|
|
405
386
|
);
|
|
406
387
|
}
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from '../../utils/log-format'
|
|
18
18
|
import { getLogPalette, getLogLevelColor, type LogPalette } from './log-palette'
|
|
19
19
|
import { copyText } from '../../utils/clipboard'
|
|
20
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
21
|
+
import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
20
22
|
import {
|
|
21
23
|
LOG_EXPORT_FORMAT_LABELS,
|
|
22
24
|
LOG_EXPORT_FORMATS,
|
|
@@ -202,6 +204,7 @@ export function LogCore({
|
|
|
202
204
|
new Set(['error', 'warn', 'info', 'debug'])
|
|
203
205
|
)
|
|
204
206
|
const [showDownloadMenu, setShowDownloadMenu] = useState(false)
|
|
207
|
+
const downloadMenu = useAnimatedUnmount(showDownloadMenu, overlayExitMs('menu'))
|
|
205
208
|
const [exportScope, setExportScope] = useState<'visible' | 'all'>('visible')
|
|
206
209
|
const [exportFormat, setExportFormat] = useState<LogExportFormat>(() => {
|
|
207
210
|
try {
|
|
@@ -210,7 +213,9 @@ export function LogCore({
|
|
|
210
213
|
} catch { return 'txt' }
|
|
211
214
|
})
|
|
212
215
|
const [showTsMenu, setShowTsMenu] = useState(false)
|
|
216
|
+
const tsMenu = useAnimatedUnmount(showTsMenu, overlayExitMs('menu'))
|
|
213
217
|
const [showStructuredMenu, setShowStructuredMenu] = useState(false)
|
|
218
|
+
const structuredMenu = useAnimatedUnmount(showStructuredMenu, overlayExitMs('menu'))
|
|
214
219
|
const [structuredMode, setStructuredMode] = useState<StructuredMode>(() => {
|
|
215
220
|
try {
|
|
216
221
|
const v = localStorage.getItem('radar-logs-structured-mode') as StructuredMode | null
|
|
@@ -297,9 +302,11 @@ export function LogCore({
|
|
|
297
302
|
() => ({ format: exportFormat, showTimestamps, showPodName }),
|
|
298
303
|
[exportFormat, showTimestamps, showPodName],
|
|
299
304
|
)
|
|
305
|
+
// Keyed on presence, not logical open, so the preview doesn't flip to
|
|
306
|
+
// "nothing to export" while the popover is still fading out.
|
|
300
307
|
const exportPreview = useMemo(
|
|
301
|
-
() =>
|
|
302
|
-
[
|
|
308
|
+
() => downloadMenu.shouldRender ? previewLogExport(exportEntries, exportOptions) : [],
|
|
309
|
+
[downloadMenu.shouldRender, exportEntries, exportOptions],
|
|
303
310
|
)
|
|
304
311
|
// Offering a scope that resolves to the same lines is a choice about nothing.
|
|
305
312
|
const scopeIsMeaningful = displayEntries.length !== bufferEntries.length
|
|
@@ -309,8 +316,12 @@ export function LogCore({
|
|
|
309
316
|
// silently exporting the whole buffer on a later visit.
|
|
310
317
|
const toggleExportMenu = useCallback(() => {
|
|
311
318
|
setShowDownloadMenu(prev => !prev)
|
|
312
|
-
setExportScope('visible')
|
|
313
319
|
}, [])
|
|
320
|
+
// The reset lands once the popover has left the DOM, not at logical close,
|
|
321
|
+
// so the preview doesn't re-scope while the popover is still fading out.
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!downloadMenu.shouldRender) setExportScope('visible')
|
|
324
|
+
}, [downloadMenu.shouldRender])
|
|
314
325
|
|
|
315
326
|
const handleExportCopy = useCallback(() => {
|
|
316
327
|
closeExportMenu()
|
|
@@ -620,8 +631,14 @@ export function LogCore({
|
|
|
620
631
|
</span>
|
|
621
632
|
</button>
|
|
622
633
|
</Tooltip>
|
|
623
|
-
{
|
|
624
|
-
<div
|
|
634
|
+
{structuredMenu.shouldRender && (
|
|
635
|
+
<div
|
|
636
|
+
inert={!showStructuredMenu}
|
|
637
|
+
className={`absolute top-full right-0 mt-1 w-56 origin-top-right ${palette.menuBg} border ${palette.border} rounded-lg shadow-lg z-50 ${TRANSITION_MENU} ${
|
|
638
|
+
structuredMenu.isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]'
|
|
639
|
+
} ${showStructuredMenu ? '' : 'pointer-events-none'}`}
|
|
640
|
+
style={overlayTransitionStyle(structuredMenu.isOpen, 'menu')}
|
|
641
|
+
>
|
|
625
642
|
<div className={`px-3 py-1.5 text-[10px] uppercase tracking-wide ${palette.textTertiary} border-b ${palette.border}`}>
|
|
626
643
|
Structured display
|
|
627
644
|
</div>
|
|
@@ -673,8 +690,14 @@ export function LogCore({
|
|
|
673
690
|
</span>
|
|
674
691
|
</button>
|
|
675
692
|
</Tooltip>
|
|
676
|
-
{
|
|
677
|
-
<div
|
|
693
|
+
{tsMenu.shouldRender && (
|
|
694
|
+
<div
|
|
695
|
+
inert={!showTsMenu}
|
|
696
|
+
className={`absolute top-full right-0 mt-1 w-44 origin-top-right ${palette.menuBg} border ${palette.border} rounded-lg shadow-lg z-50 ${TRANSITION_MENU} ${
|
|
697
|
+
tsMenu.isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]'
|
|
698
|
+
} ${showTsMenu ? '' : 'pointer-events-none'}`}
|
|
699
|
+
style={overlayTransitionStyle(tsMenu.isOpen, 'menu')}
|
|
700
|
+
>
|
|
678
701
|
<div className={`px-3 py-1.5 text-[10px] uppercase tracking-wide ${palette.textTertiary} border-b ${palette.border}`}>
|
|
679
702
|
Timestamp format
|
|
680
703
|
</div>
|
|
@@ -769,8 +792,16 @@ export function LogCore({
|
|
|
769
792
|
<Download className="w-4 h-4" />
|
|
770
793
|
</button>
|
|
771
794
|
</Tooltip>
|
|
772
|
-
{
|
|
773
|
-
<div
|
|
795
|
+
{downloadMenu.shouldRender && (
|
|
796
|
+
<div
|
|
797
|
+
role="dialog"
|
|
798
|
+
aria-label="Export logs"
|
|
799
|
+
inert={!showDownloadMenu}
|
|
800
|
+
className={`absolute top-full right-0 mt-1 w-[23rem] origin-top-right ${palette.menuBg} border ${palette.border} rounded-lg shadow-lg z-50 p-3 space-y-2.5 ${TRANSITION_MENU} ${
|
|
801
|
+
downloadMenu.isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]'
|
|
802
|
+
} ${showDownloadMenu ? '' : 'pointer-events-none'}`}
|
|
803
|
+
style={overlayTransitionStyle(downloadMenu.isOpen, 'menu')}
|
|
804
|
+
>
|
|
774
805
|
<div className="flex items-center gap-3">
|
|
775
806
|
<span id="log-export-scope-label" className={exportLabelCls}>Lines</span>
|
|
776
807
|
{scopeIsMeaningful ? (
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { useState, useMemo, useEffect, useRef, useCallback, forwardRef } from 'react'
|
|
1
|
+
import { useState, useMemo, useEffect, useRef, useCallback, useId, forwardRef } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Search,
|
|
4
|
-
ChevronDown,
|
|
5
|
-
ChevronRight,
|
|
6
4
|
Eye,
|
|
7
5
|
EyeOff,
|
|
8
6
|
Pin,
|
|
@@ -10,6 +8,7 @@ import {
|
|
|
10
8
|
X,
|
|
11
9
|
} from 'lucide-react'
|
|
12
10
|
import { clsx } from 'clsx'
|
|
11
|
+
import { Collapse, CollapseChevron, useDisclosure, disclosurePanelId } from '../ui/Collapse'
|
|
13
12
|
import type { APIResource } from '../../types'
|
|
14
13
|
import { categorizeResources, CORE_RESOURCES } from '../../utils/api-resources'
|
|
15
14
|
import { getResourceIcon } from '../../utils/resource-icons'
|
|
@@ -213,6 +212,10 @@ export function ResourcesSidebar({
|
|
|
213
212
|
useEffect(() => { persistedExpandedCategories = expandedCategories }, [expandedCategories])
|
|
214
213
|
const [showEmptyKinds, setShowEmptyKinds] = useState(false)
|
|
215
214
|
const [favoritesExpanded, setFavoritesExpanded] = useState(() => pinned.length > 0)
|
|
215
|
+
const favoritesDisclosure = useDisclosure(favoritesExpanded)
|
|
216
|
+
// Category sections are mapped inline, so they can't each call useDisclosure;
|
|
217
|
+
// one generated prefix plus the category name keeps aria-controls unique.
|
|
218
|
+
const categoryPanelBase = useId()
|
|
216
219
|
|
|
217
220
|
// Ref to selected sidebar item for scrolling into view on deeplink
|
|
218
221
|
const selectedSidebarRef = useRef<HTMLButtonElement>(null)
|
|
@@ -499,14 +502,11 @@ export function ResourcesSidebar({
|
|
|
499
502
|
{/* Favorites (pinned kinds) section — always visible */}
|
|
500
503
|
<div className="mb-2">
|
|
501
504
|
<button
|
|
505
|
+
{...favoritesDisclosure.buttonProps}
|
|
502
506
|
onClick={() => setFavoritesExpanded((v) => !v)}
|
|
503
507
|
className="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary uppercase tracking-wide"
|
|
504
508
|
>
|
|
505
|
-
{favoritesExpanded
|
|
506
|
-
<ChevronDown className="w-3 h-3" />
|
|
507
|
-
) : (
|
|
508
|
-
<ChevronRight className="w-3 h-3" />
|
|
509
|
-
)}
|
|
509
|
+
<CollapseChevron open={favoritesExpanded} className="w-3 h-3" />
|
|
510
510
|
<span className="flex-1 text-left">Favorites</span>
|
|
511
511
|
{!favoritesExpanded && pinned.length > 0 && (
|
|
512
512
|
<span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center font-mono', pinned.length < 1000 ? 'w-8' : 'w-9')}>
|
|
@@ -514,41 +514,36 @@ export function ResourcesSidebar({
|
|
|
514
514
|
</span>
|
|
515
515
|
)}
|
|
516
516
|
</button>
|
|
517
|
-
<
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
/>
|
|
546
|
-
)
|
|
547
|
-
})
|
|
548
|
-
)}
|
|
549
|
-
</div>
|
|
517
|
+
<Collapse open={favoritesExpanded} id={favoritesDisclosure.panelId}>
|
|
518
|
+
<div className="space-y-0.5">
|
|
519
|
+
{pinned.length === 0 ? (
|
|
520
|
+
<div className="px-3 py-2 text-xs text-theme-text-disabled">
|
|
521
|
+
No pinned resources. Click <Pin className="w-3 h-3 inline" /> on any resource type to pin it here.
|
|
522
|
+
</div>
|
|
523
|
+
) : (
|
|
524
|
+
pinned.map((p) => {
|
|
525
|
+
const isResourceSelected =
|
|
526
|
+
(effectiveSelectedKind.name === p.name && effectiveSelectedKind.group === p.group) ||
|
|
527
|
+
(effectiveSelectedKind.kind.toLowerCase() === p.kind.toLowerCase() && effectiveSelectedKind.group === p.group)
|
|
528
|
+
const highlighted = isKindHighlighted(p.name, p.group)
|
|
529
|
+
return (
|
|
530
|
+
<ResourceTypeButton
|
|
531
|
+
key={`${p.name}-${p.group}`}
|
|
532
|
+
ref={highlighted ? highlightedRef : (isResourceSelected ? selectedSidebarRef : null)}
|
|
533
|
+
resource={{ name: p.name, kind: p.kind, group: p.group, version: '', namespaced: true, isCrd: false, verbs: [] }}
|
|
534
|
+
count={counts[p.group ? `${p.group}/${p.kind}` : p.kind] ?? null}
|
|
535
|
+
isSelected={isResourceSelected}
|
|
536
|
+
isHighlighted={highlighted}
|
|
537
|
+
isForbidden={forbiddenKinds.has(p.group ? `${p.group}/${p.kind}` : p.kind)}
|
|
538
|
+
isPinned={true}
|
|
539
|
+
onTogglePin={() => togglePin(p)}
|
|
540
|
+
onClick={() => selectKind({ name: p.name, kind: p.kind, group: p.group })}
|
|
541
|
+
/>
|
|
542
|
+
)
|
|
543
|
+
})
|
|
544
|
+
)}
|
|
550
545
|
</div>
|
|
551
|
-
</
|
|
546
|
+
</Collapse>
|
|
552
547
|
</div>
|
|
553
548
|
{filteredCategories ? (
|
|
554
549
|
// Dynamic categories from API
|
|
@@ -558,14 +553,12 @@ export function ResourcesSidebar({
|
|
|
558
553
|
return (
|
|
559
554
|
<div key={category.name} className="mb-2">
|
|
560
555
|
<button
|
|
556
|
+
aria-expanded={isExpanded}
|
|
557
|
+
aria-controls={disclosurePanelId(categoryPanelBase, category.name)}
|
|
561
558
|
onClick={() => toggleCategory(category.name)}
|
|
562
559
|
className="w-full flex items-center gap-2 px-2 py-1.5 text-xs font-semibold text-theme-text-tertiary hover:text-theme-text-secondary uppercase tracking-wide"
|
|
563
560
|
>
|
|
564
|
-
{isExpanded
|
|
565
|
-
<ChevronDown className="w-3 h-3" />
|
|
566
|
-
) : (
|
|
567
|
-
<ChevronRight className="w-3 h-3" />
|
|
568
|
-
)}
|
|
561
|
+
<CollapseChevron open={isExpanded} className="w-3 h-3" />
|
|
569
562
|
<span className="flex-1 text-left truncate" title={rawGroupTitle}>{category.name}</span>
|
|
570
563
|
{!isExpanded && (
|
|
571
564
|
<span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center font-mono', category.total < 1000 ? 'w-8' : 'w-9')}>
|
|
@@ -573,38 +566,33 @@ export function ResourcesSidebar({
|
|
|
573
566
|
</span>
|
|
574
567
|
)}
|
|
575
568
|
</button>
|
|
576
|
-
<
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
onClick={() => selectKind({ name: resource.name, kind: resource.kind, group: resource.group })}
|
|
602
|
-
/>
|
|
603
|
-
)
|
|
604
|
-
})}
|
|
605
|
-
</div>
|
|
569
|
+
<Collapse open={isExpanded} id={disclosurePanelId(categoryPanelBase, category.name)}>
|
|
570
|
+
<div className="space-y-0.5">
|
|
571
|
+
{category.visibleResources.map((resource) => {
|
|
572
|
+
const resourceIsPinned = isPinned(resource.name, resource.group)
|
|
573
|
+
const isResourceSelected =
|
|
574
|
+
(effectiveSelectedKind.name === resource.name && effectiveSelectedKind.group === resource.group) ||
|
|
575
|
+
(effectiveSelectedKind.kind.toLowerCase() === resource.kind.toLowerCase() && effectiveSelectedKind.group === resource.group)
|
|
576
|
+
// If the resource is pinned, let the Favorites section own the highlight
|
|
577
|
+
const showSelected = isResourceSelected && !resourceIsPinned
|
|
578
|
+
const highlighted = isKindHighlighted(resource.name, resource.group)
|
|
579
|
+
return (
|
|
580
|
+
<ResourceTypeButton
|
|
581
|
+
key={resource.name}
|
|
582
|
+
ref={highlighted ? highlightedRef : (isResourceSelected ? selectedSidebarRef : null)}
|
|
583
|
+
resource={resource}
|
|
584
|
+
count={counts[resource.group ? `${resource.group}/${resource.kind}` : resource.kind] ?? null}
|
|
585
|
+
isSelected={showSelected}
|
|
586
|
+
isHighlighted={highlighted}
|
|
587
|
+
isForbidden={forbiddenKinds.has(resource.group ? `${resource.group}/${resource.kind}` : resource.kind)}
|
|
588
|
+
isPinned={resourceIsPinned}
|
|
589
|
+
onTogglePin={() => togglePin({ name: resource.name, kind: resource.kind, group: resource.group })}
|
|
590
|
+
onClick={() => selectKind({ name: resource.name, kind: resource.kind, group: resource.group })}
|
|
591
|
+
/>
|
|
592
|
+
)
|
|
593
|
+
})}
|
|
606
594
|
</div>
|
|
607
|
-
</
|
|
595
|
+
</Collapse>
|
|
608
596
|
</div>
|
|
609
597
|
)
|
|
610
598
|
})
|
|
@@ -3,6 +3,7 @@ import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
|
|
|
3
3
|
import { useDebouncedValue } from '../../hooks/useDebouncedValue'
|
|
4
4
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
5
5
|
import { RestrictedState } from '../ui/RestrictedState'
|
|
6
|
+
import { FetchResult } from '../ui/FetchResult'
|
|
6
7
|
import { Input } from '../ui/Input'
|
|
7
8
|
import type { TopPodMetrics, TopNodeMetrics, ContainerResourceMetrics } from '../../types'
|
|
8
9
|
import {
|
|
@@ -711,6 +712,23 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
711
712
|
{ key: 'metrics', label: 'Metrics', width: 'w-40', tooltip: 'Per-metric verdicts' },
|
|
712
713
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
713
714
|
],
|
|
715
|
+
analysistemplates: [
|
|
716
|
+
{ key: 'name', label: 'Name' },
|
|
717
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
718
|
+
{ key: 'metricCount', label: 'Metrics', width: 'w-24' },
|
|
719
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
720
|
+
],
|
|
721
|
+
clusteranalysistemplates: [
|
|
722
|
+
{ key: 'name', label: 'Name' },
|
|
723
|
+
{ key: 'metricCount', label: 'Metrics', width: 'w-24' },
|
|
724
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
725
|
+
],
|
|
726
|
+
experiments: [
|
|
727
|
+
{ key: 'name', label: 'Name' },
|
|
728
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
729
|
+
{ key: 'status', label: 'Phase', width: 'w-28' },
|
|
730
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
731
|
+
],
|
|
714
732
|
workflows: [
|
|
715
733
|
{ key: 'name', label: 'Name' },
|
|
716
734
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
@@ -2826,6 +2844,9 @@ const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
|
|
|
2826
2844
|
sbomreports: ['aquasecurity.github.io'],
|
|
2827
2845
|
vulnerabilityreports: ['aquasecurity.github.io'],
|
|
2828
2846
|
analysisruns: ['argoproj.io'],
|
|
2847
|
+
analysistemplates: ['argoproj.io'],
|
|
2848
|
+
clusteranalysistemplates: ['argoproj.io'],
|
|
2849
|
+
experiments: ['argoproj.io'],
|
|
2829
2850
|
applications: ['argoproj.io'],
|
|
2830
2851
|
applicationsets: ['argoproj.io'],
|
|
2831
2852
|
appprojects: ['argoproj.io'],
|
|
@@ -4987,11 +5008,13 @@ export function ResourcesView({
|
|
|
4987
5008
|
// forbidden/unavailable in counts (e.g. an informer not yet synced at counts
|
|
4988
5009
|
// time) while the list query has since returned data — show the table, not
|
|
4989
5010
|
// RestrictedState. A 403 on the list itself never carries rows, so it still
|
|
4990
|
-
// forces the restricted state.
|
|
5011
|
+
// forces the restricted state. A non-403 list error also beats the counts
|
|
5012
|
+
// entry: a kind whose sync deadline fired is reported unavailable by counts,
|
|
5013
|
+
// but the list knows it failed to load — that is not an RBAC answer.
|
|
4991
5014
|
const selectedHasRows = Array.isArray(resources) && resources.length > 0
|
|
4992
5015
|
const isSelectedForbidden =
|
|
4993
5016
|
isForbiddenError(selectedQueryError) ||
|
|
4994
|
-
(!selectedHasRows && forbiddenKinds.has(selectedKindCountKey))
|
|
5017
|
+
(!selectedHasRows && !selectedQueryError && forbiddenKinds.has(selectedKindCountKey))
|
|
4995
5018
|
|
|
4996
5019
|
// Reset sort and filters when kind changes (but not when syncing from URL navigation)
|
|
4997
5020
|
// Track previous kind to skip on mount (where the effect fires but kind hasn't actually changed)
|
|
@@ -6277,6 +6300,15 @@ export function ResourcesView({
|
|
|
6277
6300
|
/>
|
|
6278
6301
|
</div>
|
|
6279
6302
|
</div>
|
|
6303
|
+
) : selectedQueryError && !Array.isArray(resources) ? (
|
|
6304
|
+
// A failed fetch with no rows to fall back on must not reach the
|
|
6305
|
+
// empty state below — it would tell the user the cluster has none.
|
|
6306
|
+
<FetchResult
|
|
6307
|
+
loading={false}
|
|
6308
|
+
error={selectedQueryError}
|
|
6309
|
+
onRetry={selectedQuery?.refetch}
|
|
6310
|
+
className="absolute inset-0"
|
|
6311
|
+
/>
|
|
6280
6312
|
) : largeListGuard ? (
|
|
6281
6313
|
<div className="absolute inset-0 flex flex-col items-center justify-center text-theme-text-tertiary px-6 text-center">
|
|
6282
6314
|
<AlertTriangle className="w-8 h-8 text-amber-400 mb-2" />
|
|
@@ -7081,6 +7113,11 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7081
7113
|
return <RolloutCell resource={resource} column={column} />
|
|
7082
7114
|
case 'analysisruns':
|
|
7083
7115
|
return <AnalysisRunCell resource={resource} column={column} />
|
|
7116
|
+
case 'analysistemplates':
|
|
7117
|
+
case 'clusteranalysistemplates':
|
|
7118
|
+
return <AnalysisTemplateCell resource={resource} column={column} />
|
|
7119
|
+
case 'experiments':
|
|
7120
|
+
return <ExperimentCell resource={resource} column={column} />
|
|
7084
7121
|
case 'workflows':
|
|
7085
7122
|
return <WorkflowCell resource={resource} column={column} />
|
|
7086
7123
|
case 'cronworkflows':
|
|
@@ -8746,6 +8783,23 @@ function RolloutCell({ resource, column }: { resource: any; column: string }) {
|
|
|
8746
8783
|
}
|
|
8747
8784
|
}
|
|
8748
8785
|
|
|
8786
|
+
// An Experiment reports status.phase from the same AnalysisPhase vocabulary
|
|
8787
|
+
// as AnalysisRun (Successful/Running/Pending/Inconclusive/Failed/Error), so
|
|
8788
|
+
// getAnalysisRunStatus's mapping applies directly — routing here instead of
|
|
8789
|
+
// GenericCell avoids the generic phase heuristic's HEALTHY_PHASES set, which
|
|
8790
|
+
// doesn't recognize 'Successful' and treats 'Running' as healthy (neither
|
|
8791
|
+
// true for this vocabulary).
|
|
8792
|
+
function ExperimentCell({ resource, column }: { resource: any; column: string }) {
|
|
8793
|
+
switch (column) {
|
|
8794
|
+
case 'status': {
|
|
8795
|
+
const status = getAnalysisRunStatus(resource)
|
|
8796
|
+
return <span className={clsx('badge', status.color)}>{status.text}</span>
|
|
8797
|
+
}
|
|
8798
|
+
default:
|
|
8799
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
8800
|
+
}
|
|
8801
|
+
}
|
|
8802
|
+
|
|
8749
8803
|
function AnalysisRunCell({ resource, column }: { resource: any; column: string }) {
|
|
8750
8804
|
switch (column) {
|
|
8751
8805
|
case 'status': {
|
|
@@ -8784,6 +8838,17 @@ function AnalysisRunCell({ resource, column }: { resource: any; column: string }
|
|
|
8784
8838
|
}
|
|
8785
8839
|
}
|
|
8786
8840
|
|
|
8841
|
+
function AnalysisTemplateCell({ resource, column }: { resource: any; column: string }) {
|
|
8842
|
+
switch (column) {
|
|
8843
|
+
case 'metricCount': {
|
|
8844
|
+
const count = (resource.spec?.metrics || []).length
|
|
8845
|
+
return <span className="text-sm text-theme-text-secondary">{count}</span>
|
|
8846
|
+
}
|
|
8847
|
+
default:
|
|
8848
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
|
|
8787
8852
|
function WorkflowCell({ resource, column }: { resource: any; column: string }) {
|
|
8788
8853
|
switch (column) {
|
|
8789
8854
|
case 'status': {
|
|
@@ -63,8 +63,8 @@ describe('curated column ownership', () => {
|
|
|
63
63
|
// Brace-counting, not parsing: assert the extraction actually saw the whole
|
|
64
64
|
// table, or a silently-missed key would let the guard above pass vacuously.
|
|
65
65
|
it('extracted every curated key', () => {
|
|
66
|
-
expect(CURATED.length).toBe(
|
|
67
|
-
expect(OWNED.size).toBe(
|
|
66
|
+
expect(CURATED.length).toBe(239)
|
|
67
|
+
expect(OWNED.size).toBe(201)
|
|
68
68
|
expect(CURATED).toContain('pods')
|
|
69
69
|
expect(CURATED).toContain('crossplanemanagedresources')
|
|
70
70
|
expect([...OWNED]).toContain('awsmanagedcontrolplanes')
|
|
@@ -121,7 +121,7 @@ describe('every declaration resolves', () => {
|
|
|
121
121
|
.map(m => [m[1], m[2].split(',').map(g => g.trim().replace(/'/g, ''))] as const)
|
|
122
122
|
|
|
123
123
|
it('covers the whole ownership table', () => {
|
|
124
|
-
expect(DECLARED.length).toBe(
|
|
124
|
+
expect(DECLARED.length).toBe(201)
|
|
125
125
|
})
|
|
126
126
|
|
|
127
127
|
it.each(DECLARED)('%s is curated under each group it claims', (key, groups) => {
|