@skyhook-io/k8s-ui 1.19.0 → 1.20.0
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 +9 -2
- package/src/components/applications/ApplicationDetail.tsx +8 -2
- package/src/components/cluster-switcher/ClusterSwitcher.test.tsx +54 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +7 -3
- package/src/components/compare/ResourceCompareView.test.tsx +54 -0
- package/src/components/compare/ResourceCompareView.tsx +35 -12
- package/src/components/gitops/GitOpsDetailLayout.test.tsx +16 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +54 -7
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +1 -1
- package/src/components/gitops/GitOpsTableView.tsx +26 -7
- package/src/components/gitops/RollbackDialog.test.tsx +80 -0
- package/src/components/gitops/RollbackDialog.tsx +25 -6
- package/src/components/gitops/SyncOptionsDialog.render.test.tsx +374 -0
- package/src/components/gitops/SyncOptionsDialog.test.ts +54 -1
- package/src/components/gitops/SyncOptionsDialog.tsx +179 -34
- package/src/components/gitops/applicationset-row.test.ts +12 -0
- package/src/components/gitops/argo-radar-upgrade.test.tsx +369 -0
- package/src/components/gitops/detail-helpers.test.ts +8 -2
- package/src/components/gitops/detail-helpers.ts +9 -11
- package/src/components/gitops/index.ts +1 -1
- package/src/components/gitops/insights/GitOpsActivityInsightView.test.tsx +55 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +30 -12
- package/src/components/gitops/insights/GitOpsRollbackInsightView.test.tsx +73 -0
- package/src/components/gitops/insights/GitOpsSyncWindowInsightView.test.tsx +29 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +37 -0
- package/src/components/gitops/insights/insights-helpers.ts +12 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -1
- package/src/components/issues/diagnostic.ts +14 -1
- package/src/components/issues/severity.ts +14 -0
- package/src/components/kueue/AdmissionPath.tsx +223 -0
- package/src/components/kueue/QueueEvidence.tsx +148 -0
- package/src/components/kueue/constants.ts +1 -0
- package/src/components/kueue/eligibility.ts +43 -0
- package/src/components/kueue/flavors-tried.ts +79 -0
- package/src/components/kueue/helpers.test.ts +105 -0
- package/src/components/kueue/policy.ts +100 -0
- package/src/components/kueue/queue-detail.test.tsx +317 -0
- package/src/components/kueue/queue-status.ts +42 -0
- package/src/components/kueue/related-read.ts +48 -0
- package/src/components/kueue/testdata/kueue-v0.18-policy-enums.json +89 -0
- package/src/components/logs/StructuredLogLine.test.tsx +22 -0
- package/src/components/logs/StructuredLogLine.tsx +8 -3
- package/src/components/resources/KueueAdmissionSection.test.tsx +117 -0
- package/src/components/resources/KueueAdmissionSection.tsx +51 -12
- package/src/components/resources/KueuePendingPosition.test.tsx +97 -0
- package/src/components/resources/KueuePendingPosition.tsx +91 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +8 -0
- package/src/components/resources/ResourcesSidebar.tsx +3 -2
- package/src/components/resources/ResourcesView.crd.test.tsx +28 -0
- package/src/components/resources/ResourcesView.node-pods.test.tsx +25 -0
- package/src/components/resources/ResourcesView.strimzi.test.tsx +37 -0
- package/src/components/resources/ResourcesView.tsx +50 -3
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/generic-status.ts +7 -0
- package/src/components/resources/get-pod-phase-display.test.ts +70 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +54 -1
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +7 -8
- package/src/components/resources/renderers/CRDSchemaBrowser.test.tsx +105 -0
- package/src/components/resources/renderers/CRDSchemaBrowser.tsx +134 -0
- package/src/components/resources/renderers/CustomResourceDefinitionRenderer.test.tsx +106 -0
- package/src/components/resources/renderers/CustomResourceDefinitionRenderer.tsx +152 -0
- package/src/components/resources/renderers/JobRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/JobRenderer.tsx +7 -2
- package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +5 -5
- package/src/components/resources/renderers/KueueQueueRenderers.tsx +250 -170
- package/src/components/resources/renderers/KueueQueueWorkloads.test.tsx +189 -0
- package/src/components/resources/renderers/KueueQueueWorkloads.tsx +460 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +23 -52
- package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +11 -6
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +23 -5
- package/src/components/resources/renderers/ServiceAccountRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +15 -10
- package/src/components/resources/renderers/StrimziConnectorRenderer.tsx +61 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +9 -3
- package/src/components/resources/renderers/crd-cells.test.tsx +30 -0
- package/src/components/resources/renderers/crd-cells.tsx +55 -0
- package/src/components/resources/renderers/crd-schema-parts.test.tsx +59 -0
- package/src/components/resources/renderers/crd-schema-parts.tsx +121 -0
- package/src/components/resources/renderers/index.ts +6 -0
- package/src/components/resources/renderers/kueue-cells.tsx +5 -0
- package/src/components/resources/resource-utils-argo.test.ts +94 -1
- package/src/components/resources/resource-utils-argo.ts +84 -5
- package/src/components/resources/resource-utils-crd.test.ts +32 -0
- package/src/components/resources/resource-utils-crd.ts +26 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +12 -0
- package/src/components/resources/resource-utils-kueue-admission.test.ts +26 -0
- package/src/components/resources/resource-utils-kueue.test.ts +15 -0
- package/src/components/resources/resource-utils-kueue.ts +137 -4
- package/src/components/resources/resource-utils-opentelemetry.test.tsx +9 -1
- package/src/components/resources/resource-utils-opentelemetry.ts +1 -1
- package/src/components/resources/resource-utils-ray.test.ts +42 -1
- package/src/components/resources/resource-utils-ray.ts +38 -0
- package/src/components/resources/resource-utils-strimzi.test.tsx +118 -0
- package/src/components/resources/resource-utils-strimzi.ts +35 -0
- package/src/components/resources/resource-utils.ts +45 -1
- package/src/components/shared/ResourceActionsBar.tsx +83 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +37 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +40 -7
- package/src/components/topology/K8sResourceNode.test.ts +14 -0
- package/src/components/topology/K8sResourceNode.tsx +18 -1
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +96 -94
- package/src/components/topology/layout.ts +3 -1
- package/src/components/topology/pod-group-expansion.test.ts +38 -0
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/RadarUpgradeNote.tsx +8 -0
- package/src/components/ui/drawer-components.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/PodList.tsx +35 -9
- package/src/components/workload/WorkloadView.spec.test.tsx +41 -1
- package/src/components/workload/WorkloadView.tsx +61 -8
- package/src/components/workload/workload-logs-availability.test.ts +7 -0
- package/src/types/core.ts +40 -0
- package/src/types/gitops-insights.ts +11 -2
- package/src/types/gitops-tree.ts +2 -2
- package/src/types/kueue-queues.ts +84 -0
- package/src/types/read-coverage.ts +2 -1
- package/src/types/scheduling.ts +72 -0
- package/src/utils/animation.test.ts +38 -0
- package/src/utils/animation.ts +34 -0
- package/src/utils/api-resources-crd.test.ts +11 -0
- package/src/utils/api-resources.ts +5 -1
- package/src/utils/applications.test.ts +15 -1
- package/src/utils/applications.ts +26 -1
- package/src/utils/crd-schema.test.ts +107 -0
- package/src/utils/crd-schema.ts +201 -0
- package/src/utils/format.test.ts +37 -1
- package/src/utils/format.ts +28 -80
- package/src/utils/gitops-owner.test.ts +28 -2
- package/src/utils/gitops-owner.ts +19 -0
- package/src/utils/kueue-queue-workloads.ts +19 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +90 -1
- package/src/utils/topology-neighborhood.ts +55 -3
- package/src/utils/workload-history-notes.test.ts +76 -0
- package/src/utils/workload-history-notes.ts +93 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dedupeResourceRefs } from '../../utils/resource-refs'
|
|
2
|
+
import { isKubeRayClusterIdentity } from '../resources/resource-utils-ray'
|
|
2
3
|
import { PodRow, PodListFrame, PodListToggle, workloadPodDetail } from './PodList'
|
|
3
4
|
export { workloadPodDetail, podStatusLabel, podStatusClass, podDotClass } from './PodList'
|
|
4
5
|
import { useState, useMemo, useEffect, useRef, useCallback, type ComponentType, type ReactNode } from 'react'
|
|
@@ -38,10 +39,12 @@ import {
|
|
|
38
39
|
} from 'lucide-react'
|
|
39
40
|
import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
|
|
40
41
|
import type { GitOpsStatus } from '../../types/gitops'
|
|
42
|
+
import type { ReadGap } from '../../types/read-coverage'
|
|
43
|
+
import { workloadHistoryNotes, type WorkloadHistoryCoverage } from '../../utils/workload-history-notes'
|
|
41
44
|
import { isFetchError } from '../../types/fetch-error'
|
|
42
45
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
43
46
|
import { refToSelectedResource, pluralToKind, knownKindForPluralWithGroup, kindToPlural, kindToPluralWithGroup, apiVersionToGroup } from '../../utils/navigation'
|
|
44
|
-
import { neighborhoodFor, seedNodeIds, topologyNodeResourceKind } from '../../utils/topology-neighborhood'
|
|
47
|
+
import { jobSetMemberRunKey, neighborhoodFor, seedNodeIds, topologyNodeResourceKind } from '../../utils/topology-neighborhood'
|
|
45
48
|
import { TopologyGraph } from '../topology/TopologyGraph'
|
|
46
49
|
import { gitOpsOwnerFromRelationships, managerChipCoversGroup, managerChipPresentation, managerListing, type GitOpsOwnerRef } from '../../utils/gitops-owner'
|
|
47
50
|
import { gitOpsRouteForResource } from '../../utils/gitops-route'
|
|
@@ -125,8 +128,8 @@ interface WorkloadViewProps {
|
|
|
125
128
|
onCollapseToDrawer?: () => void
|
|
126
129
|
/** false = collapsed drawer mode, true (default) = full expanded mode */
|
|
127
130
|
expanded?: boolean
|
|
128
|
-
/** false on the outgoing layer during an expand/collapse crossfade
|
|
129
|
-
* keyboard shortcuts
|
|
131
|
+
/** false on the outgoing layer during an expand/collapse crossfade: suspend
|
|
132
|
+
* keyboard shortcuts and let hosted renderers pause their reads (default true). */
|
|
130
133
|
active?: boolean
|
|
131
134
|
/** Close the drawer (collapsed mode) */
|
|
132
135
|
onClose?: () => void
|
|
@@ -230,8 +233,12 @@ interface WorkloadViewProps {
|
|
|
230
233
|
/** allEvents holds only this workload's own history (what it runs and the
|
|
231
234
|
* resources attached to it), not its whole namespace. */
|
|
232
235
|
historyScoped?: boolean
|
|
233
|
-
/**
|
|
236
|
+
/** Scope bounds or expired ownership records may leave related history out. */
|
|
234
237
|
historyIncomplete?: boolean
|
|
238
|
+
/** Radar may not hold every change since the workload existed. */
|
|
239
|
+
historyCoverage?: WorkloadHistoryCoverage | null
|
|
240
|
+
/** Related kinds the history can't include for this viewer. */
|
|
241
|
+
historyGaps?: ReadGap[] | null
|
|
235
242
|
historyError?: Error | null
|
|
236
243
|
onRetryHistory?: () => void
|
|
237
244
|
/** Persisted lifecycle events reconstructed for resources related to this workload. */
|
|
@@ -360,6 +367,12 @@ interface WorkloadViewProps {
|
|
|
360
367
|
/** Place resource notices within the replacement layout's content area. */
|
|
361
368
|
leadContent?: ReactNode
|
|
362
369
|
}) => ReactNode
|
|
370
|
+
/** Called instead of navigating when a Topology-tab click lands on a member
|
|
371
|
+
* Job of the JobSet this view shows, so the host can open the member in
|
|
372
|
+
* place. `memberKey` is the member list's run key, `jobs/<namespace>/<name>`
|
|
373
|
+
* (see `jobSetMemberRunKey`), not a node ID or UID. Omit to navigate as for
|
|
374
|
+
* any node. */
|
|
375
|
+
onSelectTopologyMember?: (memberKey: string) => void
|
|
363
376
|
/** Render the metrics tab content */
|
|
364
377
|
renderMetricsTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
365
378
|
/** Render the Diagnose tab content - a path-shaped trace for network
|
|
@@ -480,6 +493,8 @@ export function WorkloadView({
|
|
|
480
493
|
olderHistoryError = null,
|
|
481
494
|
historyScoped = false,
|
|
482
495
|
historyIncomplete = false,
|
|
496
|
+
historyCoverage,
|
|
497
|
+
historyGaps,
|
|
483
498
|
historyError = null,
|
|
484
499
|
onRetryHistory,
|
|
485
500
|
relatedTimelineEvents = [],
|
|
@@ -509,6 +524,7 @@ export function WorkloadView({
|
|
|
509
524
|
renderDiagnoseTab,
|
|
510
525
|
reachableVia,
|
|
511
526
|
renderExpandedOverview,
|
|
527
|
+
onSelectTopologyMember,
|
|
512
528
|
renderSummary,
|
|
513
529
|
extraTabs,
|
|
514
530
|
tabOrder,
|
|
@@ -579,6 +595,21 @@ export function WorkloadView({
|
|
|
579
595
|
const [selectedEventId, setSelectedEventId] = useState<string | null>(null)
|
|
580
596
|
const [selectedPod, setSelectedPod] = useState<string | null>(null)
|
|
581
597
|
const [initialContainer, setInitialContainer] = useState<string | null>(null)
|
|
598
|
+
// Hosts reuse this view across resources, and a resource can be recreated
|
|
599
|
+
// under the same name: a Pod picked for one must not reach the next, not
|
|
600
|
+
// even for one render. A UID arriving with the first load is not a change.
|
|
601
|
+
const selectionIdentity = `${kind}\u0000${namespace}\u0000${name}`
|
|
602
|
+
// A host can briefly pass the previous resource with the new name.
|
|
603
|
+
const selectionUID = resource?.metadata?.name === name && (resource?.metadata?.namespace ?? '') === (namespace ?? '') ? resource?.metadata?.uid as string | undefined : undefined
|
|
604
|
+
const [selectionScope, setSelectionScope] = useState({ identity: selectionIdentity, uid: selectionUID })
|
|
605
|
+
if (selectionScope.identity !== selectionIdentity || (selectionUID && selectionScope.uid !== selectionUID)) {
|
|
606
|
+
const recreated = selectionScope.identity === selectionIdentity && selectionScope.uid !== undefined
|
|
607
|
+
setSelectionScope({ identity: selectionIdentity, uid: selectionUID })
|
|
608
|
+
if (selectionScope.identity !== selectionIdentity || recreated) {
|
|
609
|
+
setSelectedPod(null)
|
|
610
|
+
setInitialContainer(null)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
582
613
|
const [copied, setCopied] = useState<string | null>(null)
|
|
583
614
|
const [saveSuccess, setSaveSuccess] = useState(false)
|
|
584
615
|
const [recentImageSave, setRecentImageSave] = useState(false)
|
|
@@ -613,6 +644,7 @@ export function WorkloadView({
|
|
|
613
644
|
groupByApp: true,
|
|
614
645
|
})
|
|
615
646
|
}, [allEvents, topology, kind, group, namespace, name, historyScoped])
|
|
647
|
+
const historyNotes = useMemo(() => workloadHistoryNotes(historyCoverage, historyGaps), [historyCoverage, historyGaps])
|
|
616
648
|
|
|
617
649
|
// Topology tab — the seeded neighborhood around this one workload (its
|
|
618
650
|
// ownership core + attached Services/config/policies), not the whole namespace.
|
|
@@ -667,6 +699,13 @@ export function WorkloadView({
|
|
|
667
699
|
const yamlObject = yamlObjectId ? yamlObjects.find((o) => o.id === yamlObjectId) : undefined
|
|
668
700
|
const handleTopologyNodeClick = useCallback(
|
|
669
701
|
(node: TopologyNode) => {
|
|
702
|
+
const memberKey = onSelectTopologyMember && topology && neighborhoodFocusId
|
|
703
|
+
? jobSetMemberRunKey(topology, neighborhoodFocusId, node)
|
|
704
|
+
: undefined
|
|
705
|
+
if (memberKey) {
|
|
706
|
+
onSelectTopologyMember?.(memberKey)
|
|
707
|
+
return
|
|
708
|
+
}
|
|
670
709
|
const resourceKind = topologyNodeResourceKind(node)
|
|
671
710
|
if (!onNavigateToResource || !resourceKind || !node.name) return
|
|
672
711
|
const group = apiVersionToGroup(node.data?.apiVersion as string | undefined)
|
|
@@ -677,7 +716,7 @@ export function WorkloadView({
|
|
|
677
716
|
group,
|
|
678
717
|
})
|
|
679
718
|
},
|
|
680
|
-
[onNavigateToResource],
|
|
719
|
+
[onNavigateToResource, onSelectTopologyMember, topology, neighborhoodFocusId],
|
|
681
720
|
)
|
|
682
721
|
|
|
683
722
|
// Flatten events from hierarchy
|
|
@@ -896,7 +935,7 @@ export function WorkloadView({
|
|
|
896
935
|
// prominent "Diagnose" on a problem, a quiet icon when fine. Rendered via the host
|
|
897
936
|
// slot (DiagnoseCustomization); standalone Radar injects it, Hub overrides it.
|
|
898
937
|
const renderDiagnose = actionsBarProps?.renderDiagnose as
|
|
899
|
-
| ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
|
|
938
|
+
| ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint; createdAt?: string }) => ReactNode)
|
|
900
939
|
| undefined
|
|
901
940
|
const headerActions = resource && !resourceGone && renderHeaderActions?.({ resource, context: expanded ? 'expanded' : 'drawer', onNavigate: onNavigateToResource })
|
|
902
941
|
const diagnoseAction = resourceGone ? null : renderDiagnose?.({
|
|
@@ -905,6 +944,7 @@ export function WorkloadView({
|
|
|
905
944
|
namespace,
|
|
906
945
|
name,
|
|
907
946
|
health: diagnoseHealthHint(apiKind, resource),
|
|
947
|
+
createdAt: resource?.metadata?.creationTimestamp,
|
|
908
948
|
})
|
|
909
949
|
|
|
910
950
|
const showMetricsTab = isMetricsAvailable ? isMetricsAvailable(kind, resource) : false
|
|
@@ -1131,6 +1171,7 @@ export function WorkloadView({
|
|
|
1131
1171
|
)}
|
|
1132
1172
|
{overviewIntro && <div className="px-4 pt-4">{overviewIntro}</div>}
|
|
1133
1173
|
<ResourceRendererDispatch
|
|
1174
|
+
active={active}
|
|
1134
1175
|
resource={selectedResource}
|
|
1135
1176
|
data={resource}
|
|
1136
1177
|
relationships={rendererRelationships}
|
|
@@ -1357,6 +1398,7 @@ export function WorkloadView({
|
|
|
1357
1398
|
<div className="h-full min-h-0 overflow-y-auto">{specTab.render()}</div>
|
|
1358
1399
|
) : (effectiveTab === 'overview' || effectiveTab === 'spec') && (
|
|
1359
1400
|
<InfoTab
|
|
1401
|
+
active={active}
|
|
1360
1402
|
resource={resource}
|
|
1361
1403
|
selectedResource={selectedResource}
|
|
1362
1404
|
relationships={rendererRelationships}
|
|
@@ -1430,6 +1472,7 @@ export function WorkloadView({
|
|
|
1430
1472
|
olderError={olderHistoryError}
|
|
1431
1473
|
scoped={historyScoped}
|
|
1432
1474
|
incomplete={historyIncomplete}
|
|
1475
|
+
notes={historyNotes}
|
|
1433
1476
|
error={historyError}
|
|
1434
1477
|
onRetry={onRetryHistory}
|
|
1435
1478
|
selectedEventId={selectedEventId}
|
|
@@ -1738,6 +1781,7 @@ function EventsTab({
|
|
|
1738
1781
|
olderError,
|
|
1739
1782
|
scoped,
|
|
1740
1783
|
incomplete,
|
|
1784
|
+
notes = [],
|
|
1741
1785
|
error,
|
|
1742
1786
|
onRetry,
|
|
1743
1787
|
selectedEventId,
|
|
@@ -1754,6 +1798,8 @@ function EventsTab({
|
|
|
1754
1798
|
olderError?: Error | null
|
|
1755
1799
|
scoped?: boolean
|
|
1756
1800
|
incomplete?: boolean
|
|
1801
|
+
/** Coverage and access sentences from workloadHistoryNotes. */
|
|
1802
|
+
notes?: string[]
|
|
1757
1803
|
error?: Error | null
|
|
1758
1804
|
onRetry?: () => void
|
|
1759
1805
|
selectedEventId: string | null
|
|
@@ -1911,14 +1957,15 @@ function EventsTab({
|
|
|
1911
1957
|
)}
|
|
1912
1958
|
</div>
|
|
1913
1959
|
)}
|
|
1914
|
-
{(scoped || truncated || incomplete) && (
|
|
1960
|
+
{(scoped || truncated || incomplete || notes.length > 0) && (
|
|
1915
1961
|
<div className="shrink-0 flex flex-wrap items-center gap-x-2 gap-y-0.5 border-b border-theme-border bg-theme-base px-4 py-1.5 text-xs text-theme-text-secondary" role="note">
|
|
1916
1962
|
{scoped && (
|
|
1917
1963
|
<span className="text-theme-text-tertiary">
|
|
1918
1964
|
This workload, what it runs, and the Services, Ingresses, config and scalers attached to it.
|
|
1919
1965
|
</span>
|
|
1920
1966
|
)}
|
|
1921
|
-
{incomplete && <span>
|
|
1967
|
+
{incomplete && <span>Some past runs or related resources may be outside the history Radar can follow.</span>}
|
|
1968
|
+
{notes.map((note) => <span key={note}>{note}</span>)}
|
|
1922
1969
|
{truncated && <span>Showing the most recent events. Older history exists.</span>}
|
|
1923
1970
|
{truncated && olderError && (
|
|
1924
1971
|
<span className="text-[var(--color-error)]" title={olderError.message}>Couldn't load older events.</span>
|
|
@@ -1997,6 +2044,7 @@ function EventsTab({
|
|
|
1997
2044
|
// ============================================================================
|
|
1998
2045
|
|
|
1999
2046
|
function InfoTab({
|
|
2047
|
+
active,
|
|
2000
2048
|
resource,
|
|
2001
2049
|
selectedResource,
|
|
2002
2050
|
relationships,
|
|
@@ -2034,6 +2082,7 @@ function InfoTab({
|
|
|
2034
2082
|
onEvaluateCapacity,
|
|
2035
2083
|
recentImageSave,
|
|
2036
2084
|
}: {
|
|
2085
|
+
active: boolean
|
|
2037
2086
|
resource: any
|
|
2038
2087
|
selectedResource: SelectedResource
|
|
2039
2088
|
relationships?: Relationships
|
|
@@ -2089,6 +2138,7 @@ function InfoTab({
|
|
|
2089
2138
|
</div>
|
|
2090
2139
|
)}
|
|
2091
2140
|
<ResourceRendererDispatch
|
|
2141
|
+
active={active}
|
|
2092
2142
|
resource={selectedResource}
|
|
2093
2143
|
data={resource}
|
|
2094
2144
|
relationships={relationships}
|
|
@@ -2176,6 +2226,7 @@ const LOGS_TAB_WITHOUT_PODS_KINDS = new Set([
|
|
|
2176
2226
|
'clusterworkflowtemplates',
|
|
2177
2227
|
'scaledjobs',
|
|
2178
2228
|
'jobsets',
|
|
2229
|
+
'rayclusters',
|
|
2179
2230
|
'clusters',
|
|
2180
2231
|
])
|
|
2181
2232
|
const RUNTIME_WORKLOAD_OVERVIEW_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs'])
|
|
@@ -2193,6 +2244,8 @@ export function supportsLogsWithoutPods(
|
|
|
2193
2244
|
if (normalizedKind === 'jobsets') {
|
|
2194
2245
|
return group === 'jobset.x-k8s.io' && apiVersion === 'jobset.x-k8s.io/v1alpha2'
|
|
2195
2246
|
}
|
|
2247
|
+
// A KubeRay RayCluster's tab reads the Pods it owns now, not recorded ones.
|
|
2248
|
+
if (normalizedKind === 'rayclusters') return isKubeRayClusterIdentity(group, apiVersion)
|
|
2196
2249
|
// A CloudNativePG Cluster's instance Pods are its children, not related
|
|
2197
2250
|
// Pods, and its logs are resolved server-side from the Cluster itself.
|
|
2198
2251
|
if (normalizedKind === 'clusters') {
|
|
@@ -23,4 +23,11 @@ describe('supportsLogsWithoutPods', () => {
|
|
|
23
23
|
expect(supportsLogsWithoutPods('clusters', 'Cluster', undefined, 'postgresql.cnpg.io/v1')).toBe(true)
|
|
24
24
|
expect(supportsLogsWithoutPods('clusters', 'Cluster', 'cluster.x-k8s.io', 'cluster.x-k8s.io/v1beta1')).toBe(false)
|
|
25
25
|
})
|
|
26
|
+
|
|
27
|
+
it('offers a KubeRay RayCluster its owned-Pod logs without recorded Pods, but not other RayCluster kinds', () => {
|
|
28
|
+
expect(supportsLogsWithoutPods('rayclusters', 'RayCluster', 'ray.io', 'ray.io/v1')).toBe(true)
|
|
29
|
+
expect(supportsLogsWithoutPods('rayclusters', 'RayCluster', 'ray.io', undefined)).toBe(true)
|
|
30
|
+
expect(supportsLogsWithoutPods('rayclusters', 'RayCluster', 'ray.io', 'ray.io/v1alpha1')).toBe(false)
|
|
31
|
+
expect(supportsLogsWithoutPods('rayclusters', 'RayCluster', 'example.io', 'example.io/v1')).toBe(false)
|
|
32
|
+
})
|
|
26
33
|
})
|
package/src/types/core.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { Issue, IssueResourceRef } from '../components/issues/types'
|
|
1
2
|
import type { CapacityIntegrationState } from './capacity'
|
|
3
|
+
import type { ReadGap, ReadKindRef } from './read-coverage'
|
|
2
4
|
|
|
3
5
|
// Topology types matching the Go backend
|
|
4
6
|
|
|
@@ -119,6 +121,8 @@ export interface Capabilities {
|
|
|
119
121
|
configManagement?: 'local' | 'operator' | 'cloud'
|
|
120
122
|
exec: boolean // Terminal feature (pods/exec)
|
|
121
123
|
localTerminal: boolean // Local terminal available (not in-cluster, not disabled)
|
|
124
|
+
/** Shell family where a hand-off command runs; absent from older Radars. */
|
|
125
|
+
localShell?: 'powershell' | 'posix'
|
|
122
126
|
logs: boolean // Log viewer (pods/log)
|
|
123
127
|
portForward: boolean // Port forwarding (pods/portforward)
|
|
124
128
|
secrets: boolean // List secrets
|
|
@@ -129,6 +133,7 @@ export interface Capabilities {
|
|
|
129
133
|
mcpExecTools?: boolean // Full MCP registration; does not grant the caller exec permission
|
|
130
134
|
mcpReadOnly?: boolean // Full MCP endpoint is disabled; read-only endpoints remain available
|
|
131
135
|
mcpEnabled: boolean // MCP server is running
|
|
136
|
+
mcpApplicationEvidence?: boolean
|
|
132
137
|
// Karpenter discovery and NodePool read state. Optional on the wire for the
|
|
133
138
|
// same newer-frontend/older-backend reason as `deployment` below — consumers
|
|
134
139
|
// must treat absence as "unknown" and fall back to discovery signals.
|
|
@@ -165,6 +170,7 @@ export interface FeatureCapabilities {
|
|
|
165
170
|
helmSourceIdentity?: boolean
|
|
166
171
|
resourceReadCoverage?: boolean
|
|
167
172
|
searchCoverage?: boolean
|
|
173
|
+
applicationEvidence?: boolean
|
|
168
174
|
yamlReview?: boolean
|
|
169
175
|
yamlSchemas?: boolean
|
|
170
176
|
workloadImages?: boolean
|
|
@@ -179,7 +185,17 @@ export interface FeatureCapabilities {
|
|
|
179
185
|
agentSigninCheck?: boolean
|
|
180
186
|
serviceEndpointSlices?: boolean
|
|
181
187
|
trafficRecords?: boolean
|
|
188
|
+
argoRollbackSync?: boolean
|
|
189
|
+
argoSyncInheritance?: boolean
|
|
190
|
+
argoEnableSelfHealOff?: boolean
|
|
182
191
|
clusterScopedPlaceholder?: boolean
|
|
192
|
+
kueuePendingPosition?: boolean
|
|
193
|
+
// GET /api/workloads/rayclusters/{namespace}/{name}/pods. Older Radars that
|
|
194
|
+
// serve the route omit the flag; radarFeatures falls back to v1.15.0.
|
|
195
|
+
rayClusterPods?: boolean
|
|
196
|
+
// Workload Pod info marks native sidecars (containerStatuses[].sidecar).
|
|
197
|
+
podSidecarMarks?: boolean
|
|
198
|
+
kueueQueueWorkloads?: boolean
|
|
183
199
|
}
|
|
184
200
|
|
|
185
201
|
// DeploymentMode is the closed set of topologies Radar can run in.
|
|
@@ -234,6 +250,7 @@ export type CoreNodeKind =
|
|
|
234
250
|
| 'HorizontalPodAutoscaler'
|
|
235
251
|
| 'Job'
|
|
236
252
|
| 'CronJob'
|
|
253
|
+
| 'JobSet' // jobset.x-k8s.io JobSet
|
|
237
254
|
| 'Workflow'
|
|
238
255
|
| 'CronWorkflow'
|
|
239
256
|
| 'WorkflowTemplate'
|
|
@@ -876,8 +893,29 @@ export interface HPAMetricSummary {
|
|
|
876
893
|
status: string
|
|
877
894
|
}
|
|
878
895
|
|
|
896
|
+
/** Current findings from one bounded local inventory, independent of parent health.
|
|
897
|
+
* No groups or no findings never establishes absence of associations or recovery. */
|
|
898
|
+
export interface RelatedFindingsGroup {
|
|
899
|
+
/** Association anchor; each finding identifies its own subject. */
|
|
900
|
+
associatedVia: IssueResourceRef
|
|
901
|
+
/** Nonempty finite set of resource kinds selected for assessment; names, when present, lists all. */
|
|
902
|
+
kinds: { count: number; names?: ReadKindRef[] }
|
|
903
|
+
/** Authorized candidate resource snapshots, not findings or API calls. */
|
|
904
|
+
checked: number
|
|
905
|
+
/** Checked snapshots the producer could not assess, not known inactive states.
|
|
906
|
+
* checked - unknown is not a healthy count. */
|
|
907
|
+
unknown: number
|
|
908
|
+
findings?: Issue[]
|
|
909
|
+
/** Inventory-read limitations; always an array, including when empty. */
|
|
910
|
+
gaps: ReadGap[]
|
|
911
|
+
/** Findings were omitted from output; inventory read failures use gaps. */
|
|
912
|
+
truncated?: boolean
|
|
913
|
+
}
|
|
914
|
+
|
|
879
915
|
// Resource with computed relationships and optional certificate info (API response wrapper)
|
|
880
916
|
export interface ResourceWithRelationships<T = unknown> {
|
|
917
|
+
/** Optional association bundle; absence is not an all-clear. */
|
|
918
|
+
relatedFindings?: RelatedFindingsGroup[]
|
|
881
919
|
resource: T
|
|
882
920
|
relationships?: Relationships
|
|
883
921
|
certificateInfo?: SecretCertificateInfo
|
|
@@ -1738,6 +1776,8 @@ export interface ImageMetadata {
|
|
|
1738
1776
|
export interface WorkloadPodContainerInfo {
|
|
1739
1777
|
name: string
|
|
1740
1778
|
init?: boolean
|
|
1779
|
+
/** An init container with restartPolicy Always. Absent from older Radars. */
|
|
1780
|
+
sidecar?: boolean
|
|
1741
1781
|
ready: boolean
|
|
1742
1782
|
restartCount: number
|
|
1743
1783
|
}
|
|
@@ -56,6 +56,9 @@ export interface GitOpsInsightSummary {
|
|
|
56
56
|
source?: string
|
|
57
57
|
targetRevision?: string
|
|
58
58
|
lastRevision?: string
|
|
59
|
+
// Revision(s) deployed by the latest successful marked rollback, set only while
|
|
60
|
+
// effective auto-sync is off. Absence does not mean no rollback happened.
|
|
61
|
+
rollbackRevision?: string
|
|
59
62
|
lastReconcile?: string
|
|
60
63
|
partialReason?: string
|
|
61
64
|
// Human-readable sync mode for the chip in the status strip.
|
|
@@ -84,8 +87,8 @@ export interface GitOpsInsightSummary {
|
|
|
84
87
|
// controller's verdicts are not in the Application object and any
|
|
85
88
|
// per-resource health shown is Radar's own read.
|
|
86
89
|
resourceHealthMode?: GitOpsHealthMode
|
|
87
|
-
// The
|
|
88
|
-
//
|
|
90
|
+
// The current destination or recorded Argo comparison is elsewhere;
|
|
91
|
+
// Radar derives nothing from same-named local resources.
|
|
89
92
|
remoteDestination?: boolean
|
|
90
93
|
// Per-resource health came from Argo CD's API server: the verdicts shown
|
|
91
94
|
// are Argo's even though the Application object doesn't carry them.
|
|
@@ -242,6 +245,12 @@ export interface GitOpsHistoryItem {
|
|
|
242
245
|
rawMessage?: string
|
|
243
246
|
source?: string
|
|
244
247
|
initiatedBy?: string
|
|
248
|
+
// Whether an Argo CD history entry has an ID and recorded source(s), independently
|
|
249
|
+
// of auto-sync, lifecycle, in-flight operations and RBAC. Absent where not
|
|
250
|
+
// applicable (current-operation and Flux rows), or not supplied by Radar.
|
|
251
|
+
rollbackable?: boolean
|
|
252
|
+
// Compatibility warning: the current Application or history entry uses plural sources.
|
|
253
|
+
rollbackMultiSourceWarning?: boolean
|
|
245
254
|
}
|
|
246
255
|
|
|
247
256
|
export interface GitOpsCapabilities {
|
package/src/types/gitops-tree.ts
CHANGED
|
@@ -95,7 +95,7 @@ export interface GitOpsResourceTree {
|
|
|
95
95
|
// The host couldn't reach the controller's API server and is retrying;
|
|
96
96
|
// ask again now and then.
|
|
97
97
|
healthApiRetrying?: boolean
|
|
98
|
-
// The
|
|
99
|
-
//
|
|
98
|
+
// The current destination or recorded Argo comparison is elsewhere;
|
|
99
|
+
// Radar derives nothing from same-named local resources.
|
|
100
100
|
remoteDestination?: boolean
|
|
101
101
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Which Workloads belong to one Kueue queue, as Radar's
|
|
2
|
+
// GET /api/kueue/{clusterqueues/{name},localqueues/{namespace}/{name}}/workloads
|
|
3
|
+
// serializes it (internal/server/kueue_queue_workloads.go).
|
|
4
|
+
import type { ReadGapSet } from './read-coverage'
|
|
5
|
+
import type { KueueAdmissionState } from './scheduling'
|
|
6
|
+
import type { ResourceReadCoverage, ResourceReadErrorBody } from './resource-coverage'
|
|
7
|
+
|
|
8
|
+
export type KueueQueueMembership = 'submission' | 'reservation'
|
|
9
|
+
|
|
10
|
+
export interface KueueQueueWorkload {
|
|
11
|
+
namespace: string
|
|
12
|
+
name: string
|
|
13
|
+
uid: string
|
|
14
|
+
apiVersion: string
|
|
15
|
+
createdAt: string | null
|
|
16
|
+
deleting: boolean
|
|
17
|
+
/** The Workload's shared admission classification (schedulinginsight.ClassifyKueueWorkload); absent when Radar doesn't classify it. */
|
|
18
|
+
phase?: 'pending' | 'quota_reserved' | 'admitted' | 'finished' | (string & {})
|
|
19
|
+
admission?: KueueAdmissionState
|
|
20
|
+
/** Why the Workload is in this queue. */
|
|
21
|
+
membership: KueueQueueMembership[]
|
|
22
|
+
queueName?: string
|
|
23
|
+
/**
|
|
24
|
+
* Where the Workload's LocalQueue points now, when Radar read it; not where it
|
|
25
|
+
* pointed at submission. Absent when there is no queueName, the LocalQueue
|
|
26
|
+
* doesn't exist, or it wasn't read (then `localQueueUnread` is true).
|
|
27
|
+
*/
|
|
28
|
+
submission?: { localQueue: string; localQueueUID: string; clusterQueue: string }
|
|
29
|
+
/** Radar didn't read the LocalQueues in this Workload's namespace, so whether it was submitted here is unknown. Absent means false. */
|
|
30
|
+
localQueueUnread?: boolean
|
|
31
|
+
/** The ClusterQueue the Workload reports its admission in. */
|
|
32
|
+
reservation?: { clusterQueue: string }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One page of a queue's Workloads. Every count describes all members Radar read for you (not just this page), as of this page's read. */
|
|
36
|
+
export interface KueueQueueMembers {
|
|
37
|
+
items: KueueQueueWorkload[]
|
|
38
|
+
/** Members the filters keep, across all pages. */
|
|
39
|
+
matching: number
|
|
40
|
+
/**
|
|
41
|
+
* Over every member Radar read for you; submission and reservation overlap
|
|
42
|
+
* (ClusterQueue only: a LocalQueue's members all belong by submission, so its
|
|
43
|
+
* reservation count is 0). `states` counts, by admission state, the members
|
|
44
|
+
* the membership filter keeps.
|
|
45
|
+
*/
|
|
46
|
+
counts: { members: number; submission: number; reservation: number; states: Record<string, number> }
|
|
47
|
+
next?: string
|
|
48
|
+
/**
|
|
49
|
+
* The Workload read alone. `complete` doesn't make the membership complete:
|
|
50
|
+
* see `submissionUnresolved`.
|
|
51
|
+
*/
|
|
52
|
+
coverage: ResourceReadCoverage
|
|
53
|
+
/**
|
|
54
|
+
* ClusterQueue only. Namespaces whose Workloads Radar read but whose
|
|
55
|
+
* LocalQueues it didn't: only reservations place Workloads there, so while
|
|
56
|
+
* set, `counts.members`, `counts.submission` and `matching` (unless filtered
|
|
57
|
+
* to reservation) are lower bounds. Absent: no such namespace.
|
|
58
|
+
*/
|
|
59
|
+
submissionUnresolved?: ReadGapSet<string>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface KueueQueueLocalQueue {
|
|
63
|
+
namespace: string
|
|
64
|
+
name: string
|
|
65
|
+
/** The LocalQueue's own counts as its controller reports them; null when not reported. */
|
|
66
|
+
reported: { pending: number | null; reserving: number | null; admitted: number | null }
|
|
67
|
+
/** Radar read Workloads in this LocalQueue's namespace for you. When false, `workloadsGap` is the pkg/readreason code, when Radar knows it. */
|
|
68
|
+
workloadsRead: boolean
|
|
69
|
+
workloadsGap?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Each part is present or its error is: `workloads` or `workloadsError`;
|
|
74
|
+
* `localQueues` or `localQueuesError` for a ClusterQueue's first page only
|
|
75
|
+
* (later pages and LocalQueue responses carry neither, except an error).
|
|
76
|
+
*/
|
|
77
|
+
export interface KueueQueueWorkloadsResponse {
|
|
78
|
+
queue: { kind: 'ClusterQueue' | 'LocalQueue'; namespace?: string; name: string; uid: string; clusterQueue?: string }
|
|
79
|
+
workloads?: KueueQueueMembers
|
|
80
|
+
workloadsError?: ResourceReadErrorBody
|
|
81
|
+
/** At most 500 items; `total` counts every LocalQueue found. Not paged. */
|
|
82
|
+
localQueues?: { items: KueueQueueLocalQueue[]; total: number; coverage: ResourceReadCoverage }
|
|
83
|
+
localQueuesError?: ResourceReadErrorBody
|
|
84
|
+
}
|
|
@@ -38,7 +38,8 @@ export interface ReadObjectRef {
|
|
|
38
38
|
* Part of an answer Radar did not read, and why. A gap covers every
|
|
39
39
|
* combination of its axes. An axis that applies is always present, listed or
|
|
40
40
|
* as an extent; an absent axis doesn't apply to the gap (namespaces on a
|
|
41
|
-
* cluster-scoped kind). Within
|
|
41
|
+
* cluster-scoped kind). Within a single-kind answer a namespace is in at most one gap;
|
|
42
|
+
* multi-kind answers may name it for different kinds and reasons.
|
|
42
43
|
* `action` is what the code calls for, so a code the client doesn't know still
|
|
43
44
|
* says what resolves it.
|
|
44
45
|
*/
|
package/src/types/scheduling.ts
CHANGED
|
@@ -45,9 +45,81 @@ export interface SchedulingObservation {
|
|
|
45
45
|
waitingForReplacementPods?: SchedulingCondition
|
|
46
46
|
requeueState?: { count?: number; requeueAt?: string }
|
|
47
47
|
concurrentAdmission?: { parentName: string; parentRef?: SchedulingRef }
|
|
48
|
+
pendingPosition?: KueuePendingPosition
|
|
49
|
+
admission?: KueueAdmissionState
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
// Mirrors resourcecontext.KueueAdmissionFact (see its Go doc for the
|
|
54
|
+
// evidence contract: message is bounded display text, an absent stale means
|
|
55
|
+
// not known to be stale).
|
|
56
|
+
export interface KueueAdmissionFact {
|
|
57
|
+
code: string
|
|
58
|
+
// Open like code: a newer Radar may send a source this UI doesn't know.
|
|
59
|
+
source?: 'condition' | 'admission_check' | 'spec' | (string & {})
|
|
60
|
+
name?: string
|
|
61
|
+
reason?: string
|
|
62
|
+
message?: string
|
|
63
|
+
since?: string
|
|
64
|
+
stale?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Mirrors resourcecontext.KueueAdmissionState. The server is the only place
|
|
68
|
+
// codes and attention are decided; a code this UI does not know still renders
|
|
69
|
+
// verbatim. A missing fact is not evidence on its own (see the Go doc for
|
|
70
|
+
// which facts each state reports), and a missing admission block means an
|
|
71
|
+
// older Radar, not a Workload with no blocker.
|
|
72
|
+
export interface KueueAdmissionState {
|
|
73
|
+
state: string
|
|
74
|
+
attention: string
|
|
75
|
+
stale?: boolean
|
|
76
|
+
inactive?: KueueAdmissionFact
|
|
77
|
+
blocker?: KueueAdmissionFact
|
|
78
|
+
disruption?: KueueAdmissionFact
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Where Kueue's visibility API placed one pending Workload when Radar read it,
|
|
83
|
+
* as the viewer (pkg/resourcecontext/kueue_position.go). Never derived by Radar.
|
|
84
|
+
*/
|
|
85
|
+
export type KueuePendingPositionStatus =
|
|
86
|
+
| 'listed'
|
|
87
|
+
| 'not_listed'
|
|
88
|
+
| 'limit_reached'
|
|
89
|
+
| 'queue_unknown'
|
|
90
|
+
| 'no_local_queue'
|
|
91
|
+
| 'not_served'
|
|
92
|
+
| 'unsupported'
|
|
93
|
+
| 'unavailable'
|
|
94
|
+
|
|
95
|
+
export interface KueuePendingPosition {
|
|
96
|
+
status: KueuePendingPositionStatus | (string & {})
|
|
97
|
+
/** listed only: Workloads Kueue orders before this one (Kueue's 0-based position). */
|
|
98
|
+
aheadInLocalQueue?: number
|
|
99
|
+
aheadInClusterQueue?: number
|
|
100
|
+
/** Workload spec.queueName; omitted when empty, even when no request was made. */
|
|
101
|
+
localQueue?: string
|
|
102
|
+
/** Rows Kueue returned; set whenever a read succeeded. */
|
|
103
|
+
rows?: number
|
|
104
|
+
/** Requested limit; present only with a successfully decoded answer. */
|
|
105
|
+
rowLimit?: number
|
|
106
|
+
/** Supported version selected for the lookup; absent when none was selected. */
|
|
107
|
+
apiVersion?: string
|
|
108
|
+
/** When Radar made the read (RFC 3339). */
|
|
109
|
+
readAt?: string
|
|
110
|
+
/** pkg/readreason code; unavailable only. */
|
|
111
|
+
code?: string
|
|
112
|
+
message?: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** GET /api/kueue/position/{namespace}/{name} */
|
|
116
|
+
export interface KueuePositionResponse {
|
|
117
|
+
uid: string
|
|
118
|
+
/** The supplied Workload snapshot permits a lookup; not proof of queue membership. */
|
|
119
|
+
eligible: boolean
|
|
120
|
+
position?: KueuePendingPosition
|
|
121
|
+
}
|
|
122
|
+
|
|
51
123
|
export interface KueueAdmissionWorkload {
|
|
52
124
|
apiVersion: string
|
|
53
125
|
namespace: string
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
DURATION_HOVER_REVEAL_IN,
|
|
4
|
+
DURATION_HOVER_REVEAL_OUT,
|
|
5
|
+
HOVER_INTENT_DELAY,
|
|
6
|
+
HOVER_REVEAL_ACTION,
|
|
7
|
+
HOVER_REVEAL_HIT,
|
|
8
|
+
HOVER_REVEAL_REST,
|
|
9
|
+
} from './animation'
|
|
10
|
+
|
|
11
|
+
// The presets spell their timing out for Tailwind's scanner; this keeps the
|
|
12
|
+
// literals in step with the named constants.
|
|
13
|
+
describe('hover-reveal presets', () => {
|
|
14
|
+
const intent = `delay-${HOVER_INTENT_DELAY}`
|
|
15
|
+
const into = `duration-${DURATION_HOVER_REVEAL_IN}`
|
|
16
|
+
const outOf = `duration-${DURATION_HOVER_REVEAL_OUT}`
|
|
17
|
+
|
|
18
|
+
it.each([
|
|
19
|
+
['action', HOVER_REVEAL_ACTION],
|
|
20
|
+
['hit target', HOVER_REVEAL_HIT],
|
|
21
|
+
])('the %s leaves on the out clock and comes in on the in clock after the intent delay', (_, preset) => {
|
|
22
|
+
const classes = preset.split(/\s+/)
|
|
23
|
+
expect(classes).toContain(outOf)
|
|
24
|
+
for (const v of ['group-hover', 'group-has-[:focus-visible]']) {
|
|
25
|
+
expect(classes).toContain(`${v}:${into}`)
|
|
26
|
+
expect(classes).toContain(`${v}:${intent}`)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('the resting content steps out on the out clock and returns on the in clock', () => {
|
|
31
|
+
const classes = HOVER_REVEAL_REST.split(/\s+/)
|
|
32
|
+
expect(classes).toContain(into)
|
|
33
|
+
for (const v of ['group-hover', 'group-has-[:focus-visible]']) {
|
|
34
|
+
expect(classes).toContain(`${v}:${outOf}`)
|
|
35
|
+
expect(classes).toContain(`${v}:${intent}`)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
})
|
package/src/utils/animation.ts
CHANGED
|
@@ -45,6 +45,17 @@ export const DURATION_DISCLOSURE = 300
|
|
|
45
45
|
* should feel immediate. */
|
|
46
46
|
export const DURATION_MENU_IN = 140
|
|
47
47
|
export const DURATION_MENU_OUT = 100
|
|
48
|
+
/** Hover-revealed row actions (HOVER_REVEAL_*). In the range of Radar's other
|
|
49
|
+
* hover effects (150–200ms) rather than the menu clock, which reads as a
|
|
50
|
+
* flicker on something the pointer merely passes over. The exit is shorter
|
|
51
|
+
* so leaving a row feels immediate. The preset class strings spell these
|
|
52
|
+
* values out for Tailwind's scanner; a test keeps them in step. */
|
|
53
|
+
export const DURATION_HOVER_REVEAL_IN = 200
|
|
54
|
+
export const DURATION_HOVER_REVEAL_OUT = 120
|
|
55
|
+
/** Hover intent before a row action comes in: a pointer sweeping down a long
|
|
56
|
+
* list passes each row in well under this, so actions don't ripple in behind
|
|
57
|
+
* it. Entrance only; leaving a row hides its action at once. */
|
|
58
|
+
export const HOVER_INTENT_DELAY = 60
|
|
48
59
|
/** Modal dialogs and command palettes. */
|
|
49
60
|
export const DURATION_DIALOG_IN = 220
|
|
50
61
|
export const DURATION_DIALOG_OUT = 160
|
|
@@ -104,6 +115,29 @@ export const TRANSITION_PANEL =
|
|
|
104
115
|
export const TRANSITION_MENU =
|
|
105
116
|
`transition-[translate,scale,opacity] ${TW_EASE} motion-reduce:transition-none`
|
|
106
117
|
|
|
118
|
+
/** Hover-revealed row action, the resting half: the row content the action
|
|
119
|
+
* replaces (an age, a chip) lifts 3px and fades on the exit clock. Needs a
|
|
120
|
+
* `group` ancestor; keyboard reveal keys on :focus-visible inside the group,
|
|
121
|
+
* so a row left focused by a mouse click doesn't keep its action up. Timing
|
|
122
|
+
* is DURATION_HOVER_REVEAL_IN / _OUT and HOVER_INTENT_DELAY. */
|
|
123
|
+
export const HOVER_REVEAL_REST =
|
|
124
|
+
`transition-[opacity,translate] duration-200 ${TW_EASE} motion-reduce:transition-none group-hover:opacity-0 group-hover:-translate-y-[3px] group-hover:duration-120 group-hover:delay-60 group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:-translate-y-[3px] group-has-[:focus-visible]:duration-120 group-has-[:focus-visible]:delay-60`
|
|
125
|
+
|
|
126
|
+
/** Hover-revealed row action, the action half: rises 3px into place on the
|
|
127
|
+
* entrance clock after the hover-intent delay, drops out on the exit clock.
|
|
128
|
+
* Never a hit target itself, so its padding and any backdrop leave clicks to
|
|
129
|
+
* the row underneath; put HOVER_REVEAL_HIT on the control inside it. Pair
|
|
130
|
+
* with HOVER_REVEAL_REST in one group. */
|
|
131
|
+
export const HOVER_REVEAL_ACTION =
|
|
132
|
+
`transition-[opacity,translate] duration-120 ${TW_EASE} motion-reduce:transition-none pointer-events-none opacity-0 translate-y-[3px] group-hover:opacity-100 group-hover:translate-y-0 group-hover:duration-200 group-hover:delay-60 group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:translate-y-0 group-has-[:focus-visible]:duration-200 group-has-[:focus-visible]:delay-60`
|
|
133
|
+
|
|
134
|
+
/** Hover-revealed row action, the control inside it: takes clicks only while
|
|
135
|
+
* shown. pointer-events transitions as a discrete property on the same
|
|
136
|
+
* clock, so it flips partway through the entrance rather than at hover start
|
|
137
|
+
* and a click during the intent delay lands on the row. */
|
|
138
|
+
export const HOVER_REVEAL_HIT =
|
|
139
|
+
`transition-[pointer-events] transition-discrete duration-120 ${TW_EASE} motion-reduce:transition-none pointer-events-none group-hover:pointer-events-auto group-hover:duration-200 group-hover:delay-60 group-has-[:focus-visible]:pointer-events-auto group-has-[:focus-visible]:duration-200 group-has-[:focus-visible]:delay-60`
|
|
140
|
+
|
|
107
141
|
export type OverlayKind = 'menu' | 'dialog' | 'sheet' | 'drawer'
|
|
108
142
|
|
|
109
143
|
const OVERLAY_DURATIONS: Record<OverlayKind, { in: number; out: number }> = {
|