@skyhook-io/k8s-ui 1.14.9 → 1.14.11
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 +3 -2
- package/src/components/applications/ApplicationDetail.tsx +24 -11
- package/src/components/audit/AuditAlerts.tsx +1 -3
- package/src/components/audit/AuditCoveragePresentation.test.tsx +48 -2
- package/src/components/checks/ChecksView.test.tsx +55 -0
- package/src/components/checks/ChecksView.tsx +38 -27
- package/src/components/dock/NodeTerminalTab.test.tsx +89 -0
- package/src/components/dock/NodeTerminalTab.tsx +51 -47
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +17 -9
- package/src/components/gitops/detail-helpers.test.ts +32 -0
- package/src/components/gitops/detail-helpers.ts +21 -0
- package/src/components/gitops/health-provenance.test.ts +4 -3
- package/src/components/gitops/index.ts +2 -1
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +131 -54
- package/src/components/issues/IssuesView.tsx +6 -0
- package/src/components/issues/ResourceIssuesSection.test.tsx +36 -0
- package/src/components/issues/diagnostic.ts +4 -0
- package/src/components/logs/LogCore.tsx +3 -3
- package/src/components/logs/WorkloadLogsViewer.tsx +41 -11
- package/src/components/logs/useLogBuffer.ts +1 -0
- package/src/components/logs/useLogStream.ts +1 -2
- package/src/components/resources/KueueAdmissionSection.test.tsx +95 -0
- package/src/components/resources/KueueAdmissionSection.tsx +87 -0
- package/src/components/resources/ResourcesView.default-sort.test.ts +26 -0
- package/src/components/resources/ResourcesView.tsx +91 -16
- package/src/components/resources/curated-column-ownership.test.ts +45 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +4 -3
- package/src/components/resources/renderers/ContainerEnvironmentSection.tsx +10 -3
- package/src/components/resources/renderers/JobSetRenderer.test.tsx +280 -0
- package/src/components/resources/renderers/JobSetRenderer.tsx +322 -0
- package/src/components/resources/renderers/KueueProvisioningRenderers.test.tsx +67 -0
- package/src/components/resources/renderers/KueueProvisioningRenderers.tsx +91 -0
- package/src/components/resources/renderers/KueueQueueNavigation.test.tsx +69 -0
- package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +123 -0
- package/src/components/resources/renderers/KueueQueueRenderers.tsx +299 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +121 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +230 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +93 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +62 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +114 -0
- package/src/components/resources/renderers/index.ts +11 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +103 -2
- package/src/components/resources/resource-utils-jobset-lws.ts +43 -9
- package/src/components/resources/resource-utils-kueue.ts +145 -50
- package/src/components/resources/resource-utils-ray.test.ts +58 -1
- package/src/components/resources/resource-utils-ray.ts +56 -90
- package/src/components/shared/ResourceRendererDispatch.test.tsx +117 -2
- package/src/components/shared/ResourceRendererDispatch.tsx +47 -10
- package/src/components/timeline/TimelineList.tsx +5 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +15 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +43 -23
- package/src/components/topology/K8sResourceNode.tsx +3 -3
- package/src/components/ui/CodeViewer.tsx +4 -0
- package/src/components/ui/Toast.tsx +9 -3
- package/src/components/ui/drawer-components.tsx +3 -1
- package/src/components/workload/PodList.tsx +154 -0
- package/src/components/workload/WorkloadView.tsx +29 -159
- package/src/components/workload/index.ts +11 -1
- package/src/components/workload/workload-logs-availability.test.ts +20 -0
- package/src/components/workload/workload-pod-presentation.test.ts +27 -0
- package/src/types/core.ts +24 -0
- package/src/types/gitops-tree.ts +5 -2
- package/src/types/scheduling.ts +71 -0
- package/src/utils/api-resources.test.ts +20 -1
- package/src/utils/api-resources.ts +26 -0
- package/src/utils/application-topology.test.ts +25 -0
- package/src/utils/application-topology.ts +2 -1
- package/src/utils/applications.test.ts +82 -0
- package/src/utils/applications.ts +62 -24
- package/src/utils/gitops-route.test.ts +20 -1
- package/src/utils/gitops-route.ts +15 -7
- package/src/utils/log-format.ts +5 -2
- package/src/utils/log-stream-error.test.ts +13 -0
- package/src/utils/navigation.test.ts +2 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/resource-hierarchy.test.ts +402 -15
- package/src/utils/resource-hierarchy.ts +263 -178
- package/src/utils/topology-neighborhood.test.ts +72 -1
- package/src/utils/topology-neighborhood.ts +40 -18
|
@@ -46,7 +46,8 @@ import { MiddleEllipsis } from '../ui/MiddleEllipsis'
|
|
|
46
46
|
import { CollapseChevron } from '../ui/Collapse'
|
|
47
47
|
import { Tooltip } from '../ui/Tooltip'
|
|
48
48
|
import { ResourceRefBadge } from '../ui/drawer-components'
|
|
49
|
-
import { buildResourceHierarchy, extractPinnedLanes, removePinnedLanes, isProblematicEvent, laneTrackEvents, isChildVisibleInWindow, collidingLaneKeys, laneCollisionKey, type ResourceLane as BaseResourceLane, type TimelineGrouping, type PinnedLaneRef } from '../../utils/resource-hierarchy'
|
|
49
|
+
import { buildResourceHierarchy, extractPinnedLanes, removePinnedLanes, resolvePinnedLaneIds, isProblematicEvent, laneTrackEvents, isChildVisibleInWindow, collidingLaneKeys, laneCollisionKey, type ResourceLane as BaseResourceLane, type TimelineGrouping, type PinnedLaneRef } from '../../utils/resource-hierarchy'
|
|
50
|
+
import { builtinGroupForKind } from '../../utils/api-resources'
|
|
50
51
|
import { groupQualifiesLaneId } from '../../utils/navigation'
|
|
51
52
|
import type { AppMembershipIndex } from '../../utils/applications'
|
|
52
53
|
import { Layers } from 'lucide-react'
|
|
@@ -765,8 +766,8 @@ export function TimelineSwimlanes({ events, isLoading, onResourceClick, viewMode
|
|
|
765
766
|
// deep-link to GitOps detail rather than the resource drawer — the lane is
|
|
766
767
|
// already telling the user "this controller had changes/events"; the GitOps
|
|
767
768
|
// tab is the right place to investigate further.
|
|
768
|
-
const handleLaneOpen = useCallback((kind: string, namespace: string, name: string, group?: string) => {
|
|
769
|
-
const gitOpsPath = gitOpsRouteForKind(kind, namespace, name)
|
|
769
|
+
const handleLaneOpen = useCallback((kind: string, namespace: string, name: string, group?: string, identityResolved?: boolean) => {
|
|
770
|
+
const gitOpsPath = gitOpsRouteForKind(kind, namespace, name, group, identityResolved)
|
|
770
771
|
if (gitOpsPath && onNavigatePath) {
|
|
771
772
|
onNavigatePath(gitOpsPath)
|
|
772
773
|
return
|
|
@@ -1188,7 +1189,10 @@ export function TimelineSwimlanes({ events, isLoading, onResourceClick, viewMode
|
|
|
1188
1189
|
|
|
1189
1190
|
// Pin MOVES a row: pinned lanes (and pinned children inside groups) leave
|
|
1190
1191
|
// the regular list entirely — the pinned section is their only home.
|
|
1191
|
-
const pinnedIdSetForFilter = useMemo(
|
|
1192
|
+
const pinnedIdSetForFilter = useMemo(
|
|
1193
|
+
() => resolvePinnedLaneIds(lanes, pinnedLanes ?? []),
|
|
1194
|
+
[lanes, pinnedLanes],
|
|
1195
|
+
)
|
|
1192
1196
|
const pinnedAppKeys = useMemo(
|
|
1193
1197
|
() => new Set((pinnedLanes ?? []).flatMap((p) => (p.type === 'appGroup' ? [p.appKey] : []))),
|
|
1194
1198
|
[pinnedLanes],
|
|
@@ -1233,16 +1237,16 @@ export function TimelineSwimlanes({ events, isLoading, onResourceClick, viewMode
|
|
|
1233
1237
|
return n
|
|
1234
1238
|
}, [pinnedLaneRows, visibleWindow])
|
|
1235
1239
|
|
|
1236
|
-
const pinnedIdSet =
|
|
1240
|
+
const pinnedIdSet = pinnedIdSetForFilter
|
|
1237
1241
|
// A pin button for a lane, or null when the host wired no pin handler. A pinned
|
|
1238
1242
|
// lane's button is filled and always visible (in the pinned section or its
|
|
1239
1243
|
// original spot); an unpinned one reveals on row hover.
|
|
1240
1244
|
const renderPinButton = useCallback((lane: ResourceLane): React.ReactNode => {
|
|
1241
|
-
if (!onTogglePin) return null
|
|
1245
|
+
if (!onTogglePin || lane.identityAmbiguous) return null
|
|
1242
1246
|
const pinned = pinnedIdSet.has(lane.id)
|
|
1243
1247
|
const ref: PinnedLaneRef = lane.isAppGroup && lane.appKey
|
|
1244
1248
|
? { type: 'appGroup', id: lane.id, appKey: lane.appKey, appName: lane.title ?? lane.name }
|
|
1245
|
-
: { id: lane.id, kind: lane.kind, namespace: lane.namespace, name: lane.name }
|
|
1249
|
+
: { id: lane.id, kind: lane.kind, group: lane.group, namespace: lane.namespace, name: lane.name }
|
|
1246
1250
|
return (
|
|
1247
1251
|
<PinButton
|
|
1248
1252
|
pinned={pinned}
|
|
@@ -1659,10 +1663,10 @@ export function TimelineSwimlanes({ events, isLoading, onResourceClick, viewMode
|
|
|
1659
1663
|
chip + namespace. Only the NAME navigates; the rest is inert. */
|
|
1660
1664
|
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
1661
1665
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
1662
|
-
<Tooltip content={lane.name} wrapperClassName="min-w-0 flex-1">
|
|
1666
|
+
<Tooltip content={lane.identityAmbiguous ? `${lane.name} · API group unknown` : lane.name} wrapperClassName="min-w-0 flex-1">
|
|
1663
1667
|
<span
|
|
1664
|
-
onClick={() => handleLaneOpen(lane.kind, lane.namespace, lane.name, lane.group)}
|
|
1665
|
-
className={clsx('min-w-0 w-full text-sm text-theme-text-primary hover:text-accent-text hover:underline cursor-pointer', compact ? 'font-medium' : 'font-semibold font-mono')}
|
|
1668
|
+
onClick={lane.identityAmbiguous ? undefined : () => handleLaneOpen(lane.kind, lane.namespace, lane.name, lane.group, lane.identityResolved)}
|
|
1669
|
+
className={clsx('min-w-0 w-full text-sm text-theme-text-primary', !lane.identityAmbiguous && 'hover:text-accent-text hover:underline cursor-pointer', compact ? 'font-medium' : 'font-semibold font-mono')}
|
|
1666
1670
|
>
|
|
1667
1671
|
<MiddleEllipsis text={lane.name} className="block" />
|
|
1668
1672
|
</span>
|
|
@@ -1715,7 +1719,7 @@ export function TimelineSwimlanes({ events, isLoading, onResourceClick, viewMode
|
|
|
1715
1719
|
hasChildren={hasVisibleChildren}
|
|
1716
1720
|
expanded={isExpanded}
|
|
1717
1721
|
onToggle={hasVisibleChildren ? () => toggleLane(lane.id) : undefined}
|
|
1718
|
-
onClick={() => handleLaneOpen(lane.kind, lane.namespace, lane.name, lane.group)}
|
|
1722
|
+
onClick={lane.identityAmbiguous ? undefined : () => handleLaneOpen(lane.kind, lane.namespace, lane.name, lane.group, lane.identityResolved)}
|
|
1719
1723
|
pinButton={renderPinButton(lane)}
|
|
1720
1724
|
title={
|
|
1721
1725
|
lane.nestedByContract ? `${lane.name} · linked by naming`
|
|
@@ -2116,6 +2120,7 @@ export function TimelineSwimlanes({ events, isLoading, onResourceClick, viewMode
|
|
|
2116
2120
|
onClose={closeDrawer}
|
|
2117
2121
|
onResourceClick={onResourceClick}
|
|
2118
2122
|
allEvents={filteredEvents}
|
|
2123
|
+
resourceLanes={lanes}
|
|
2119
2124
|
/>
|
|
2120
2125
|
)}
|
|
2121
2126
|
</div>
|
|
@@ -2251,7 +2256,7 @@ function GroupChip({ group }: { group: string }) {
|
|
|
2251
2256
|
)
|
|
2252
2257
|
}
|
|
2253
2258
|
|
|
2254
|
-
function ChildLaneLabel({ kind, group, showGroupChip, kindTitle, name, labelWidthClass = 'w-[360px]', isLast, onClick, pinButton, title, depth = 1, hasChildren, expanded, onToggle }: { kind: string; group?: string; showGroupChip?: boolean; kindTitle?: string; name: string; labelWidthClass?: string; isLast: boolean; onClick
|
|
2259
|
+
function ChildLaneLabel({ kind, group, showGroupChip, kindTitle, name, labelWidthClass = 'w-[360px]', isLast, onClick, pinButton, title, depth = 1, hasChildren, expanded, onToggle }: { kind: string; group?: string; showGroupChip?: boolean; kindTitle?: string; name: string; labelWidthClass?: string; isLast: boolean; onClick?: () => void; pinButton?: React.ReactNode; title?: string; depth?: number; hasChildren?: boolean; expanded?: boolean; onToggle?: () => void }) {
|
|
2255
2260
|
// Tree rails: the INCOMING trunk sits under the parent's chevron (rail d-1), the
|
|
2256
2261
|
// row's own chevron sits on its CHILDREN's rail (rail d). Deriving both from one
|
|
2257
2262
|
// ROOT keeps every level's vertical aligned under the chevron above it.
|
|
@@ -2293,7 +2298,7 @@ function ChildLaneLabel({ kind, group, showGroupChip, kindTitle, name, labelWidt
|
|
|
2293
2298
|
<Tooltip content={title ?? name} wrapperClassName="min-w-0 flex-1">
|
|
2294
2299
|
<span
|
|
2295
2300
|
onClick={onClick}
|
|
2296
|
-
className="min-w-0 w-full text-[13px] font-mono text-theme-text-secondary hover:text-accent-text hover:underline cursor-pointer"
|
|
2301
|
+
className={clsx("min-w-0 w-full text-[13px] font-mono text-theme-text-secondary", onClick && "hover:text-accent-text hover:underline cursor-pointer")}
|
|
2297
2302
|
>
|
|
2298
2303
|
<MiddleEllipsis text={name} className="block" />
|
|
2299
2304
|
</span>
|
|
@@ -3003,6 +3008,7 @@ interface EventDetailPanelProps {
|
|
|
3003
3008
|
// Every event in view — powers the ±15-min correlation feed ("what else
|
|
3004
3009
|
// happened around this deploy?"). Absent → the correlation section is omitted.
|
|
3005
3010
|
allEvents?: TimelineEvent[]
|
|
3011
|
+
resourceLanes?: BaseResourceLane[]
|
|
3006
3012
|
}
|
|
3007
3013
|
|
|
3008
3014
|
// The ±15-min window a single event's rail pulls correlated neighbours from
|
|
@@ -3044,7 +3050,7 @@ function ClusterEventRow({ event, active, onClick }: { event: TimelineEvent; act
|
|
|
3044
3050
|
)
|
|
3045
3051
|
}
|
|
3046
3052
|
|
|
3047
|
-
export function EventDetailPanel({ events, selectedId, onSelectId, onClose, onResourceClick, allEvents }: EventDetailPanelProps) {
|
|
3053
|
+
export function EventDetailPanel({ events, selectedId, onSelectId, onClose, onResourceClick, allEvents, resourceLanes }: EventDetailPanelProps) {
|
|
3048
3054
|
// ONE drawer anatomy: rail left, detail right — for one event or
|
|
3049
3055
|
// fifty. The shape never changes with count, so muscle memory holds. A single
|
|
3050
3056
|
// clicked dot renders as a rail of one plus its ±15-min correlated neighbors,
|
|
@@ -3094,16 +3100,30 @@ export function EventDetailPanel({ events, selectedId, onSelectId, onClose, onRe
|
|
|
3094
3100
|
// Index where correlated neighbors start (single-origin rails only) — a
|
|
3095
3101
|
// divider separates "the dot you clicked" from "what happened around it".
|
|
3096
3102
|
const neighborsFrom = events.length === 1 && railEvents.length > 1 ? 1 : -1
|
|
3097
|
-
const
|
|
3103
|
+
const selectedLane = useMemo(() => {
|
|
3104
|
+
const pending = [...(resourceLanes ?? [])];
|
|
3105
|
+
while (pending.length > 0) {
|
|
3106
|
+
const lane = pending.pop()!;
|
|
3107
|
+
if (lane.events.some(event => event.id === selected.id)) return lane;
|
|
3108
|
+
pending.push(...(lane.children ?? []));
|
|
3109
|
+
}
|
|
3110
|
+
return undefined;
|
|
3111
|
+
}, [resourceLanes, selected.id]);
|
|
3112
|
+
const group = selected.apiVersion
|
|
3113
|
+
? apiVersionToGroup(selected.apiVersion)
|
|
3114
|
+
: selectedLane?.identityResolved && selectedLane.kind === selected.kind
|
|
3115
|
+
? selectedLane.group
|
|
3116
|
+
: builtinGroupForKind(selected.kind);
|
|
3117
|
+
const canOpenResource = !selectedLane?.identityAmbiguous && group !== undefined;
|
|
3118
|
+
const openResource = () => {
|
|
3119
|
+
if (!canOpenResource) return;
|
|
3098
3120
|
onResourceClick?.({
|
|
3099
|
-
kind: kindToPluralWithGroup(
|
|
3100
|
-
selected.kind,
|
|
3101
|
-
apiVersionToGroup(selected.apiVersion),
|
|
3102
|
-
),
|
|
3121
|
+
kind: kindToPluralWithGroup(selected.kind, group ?? ''),
|
|
3103
3122
|
namespace: selected.namespace,
|
|
3104
3123
|
name: selected.name,
|
|
3105
|
-
group
|
|
3106
|
-
})
|
|
3124
|
+
group,
|
|
3125
|
+
});
|
|
3126
|
+
};
|
|
3107
3127
|
|
|
3108
3128
|
return (
|
|
3109
3129
|
<div
|
|
@@ -3205,8 +3225,8 @@ export function EventDetailPanel({ events, selectedId, onSelectId, onClose, onRe
|
|
|
3205
3225
|
{/* The ONE click target for the resource — the same badge used by
|
|
3206
3226
|
every Radar drawer, so it reads as "this navigates". */}
|
|
3207
3227
|
<ResourceRefBadge
|
|
3208
|
-
resourceRef={{ kind: selected.kind || 'Event', namespace: selected.namespace ?? '', name: selected.name }}
|
|
3209
|
-
onClick={onResourceClick ? openResource : undefined}
|
|
3228
|
+
resourceRef={{ kind: selected.kind || 'Event', namespace: selected.namespace ?? '', name: selected.name, group }}
|
|
3229
|
+
onClick={onResourceClick && canOpenResource ? openResource : undefined}
|
|
3210
3230
|
/>
|
|
3211
3231
|
{selected.namespace && <span className="text-xs text-theme-text-tertiary">in {selected.namespace}</span>}
|
|
3212
3232
|
</dd>
|
|
@@ -130,10 +130,10 @@ function getIssueTooltip(issue: string | undefined): React.ReactNode {
|
|
|
130
130
|
action: "Check the webhook policy that denied the request.",
|
|
131
131
|
},
|
|
132
132
|
WebhookUnavailable: {
|
|
133
|
-
title: "Admission Webhook
|
|
133
|
+
title: "Admission Webhook Call Failed",
|
|
134
134
|
description:
|
|
135
|
-
"Pod creation
|
|
136
|
-
action: "
|
|
135
|
+
"Pod creation was blocked because an admission webhook call failed.",
|
|
136
|
+
action: "Check the reported error, webhook connectivity, TLS, and backend health.",
|
|
137
137
|
},
|
|
138
138
|
Evicted: {
|
|
139
139
|
title: "Pod Evicted",
|
|
@@ -301,6 +301,10 @@ export function CodeViewer({
|
|
|
301
301
|
const handleSearchKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
302
302
|
if (e.key === 'Escape') {
|
|
303
303
|
e.preventDefault()
|
|
304
|
+
// A host that also listens for Escape to close something bigger around
|
|
305
|
+
// this viewer (a full-screen overlay, say) must not see this one — the
|
|
306
|
+
// user closing the search bar, not the whole surface it's embedded in.
|
|
307
|
+
e.stopPropagation()
|
|
304
308
|
closeSearch()
|
|
305
309
|
} else if (e.key === 'Enter') {
|
|
306
310
|
e.preventDefault()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'
|
|
2
2
|
import { DURATION_TOAST_EXIT } from '../../utils/animation'
|
|
3
|
-
import { Check, Terminal, X, AlertTriangle } from 'lucide-react'
|
|
3
|
+
import { Check, Terminal, X, AlertTriangle, Info } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
|
|
6
6
|
interface Toast {
|
|
@@ -172,6 +172,7 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
|
|
|
172
172
|
|
|
173
173
|
const isError = toast.type === 'error'
|
|
174
174
|
const isSuccess = toast.type === 'success'
|
|
175
|
+
const isNotice = toast.type === 'info' || toast.type === 'warning'
|
|
175
176
|
|
|
176
177
|
return (
|
|
177
178
|
<div
|
|
@@ -191,10 +192,15 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
|
|
|
191
192
|
<div className={clsx(
|
|
192
193
|
'w-8 h-8 rounded-full flex items-center justify-center shrink-0',
|
|
193
194
|
isError ? 'bg-red-500/20' :
|
|
194
|
-
isSuccess ? 'bg-emerald-500/20' :
|
|
195
|
+
isSuccess ? 'bg-emerald-500/20' :
|
|
196
|
+
toast.type === 'warning' ? 'bg-amber-500/20' : 'bg-blue-500/20'
|
|
195
197
|
)}>
|
|
196
198
|
{isError ? (
|
|
197
199
|
<AlertTriangle className="w-4 h-4 text-red-400" />
|
|
200
|
+
) : toast.type === 'warning' ? (
|
|
201
|
+
<AlertTriangle className="w-4 h-4 text-amber-400" />
|
|
202
|
+
) : toast.type === 'info' ? (
|
|
203
|
+
<Info className="w-4 h-4 text-blue-400" />
|
|
198
204
|
) : toast.command ? (
|
|
199
205
|
<Terminal className={clsx('w-4 h-4', isSuccess ? 'text-emerald-400' : 'text-blue-400')} />
|
|
200
206
|
) : (
|
|
@@ -208,7 +214,7 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
|
|
|
208
214
|
<span className={clsx('text-sm font-medium', isError ? 'text-red-200' : isSuccess ? 'text-emerald-50' : 'text-theme-text-primary')}>
|
|
209
215
|
{toast.message}
|
|
210
216
|
</span>
|
|
211
|
-
{!isError && !toast.action && <Check className={clsx('w-3.5 h-3.5 shrink-0', isSuccess ? 'text-emerald-400' : 'text-green-400')} />}
|
|
217
|
+
{!isError && !isNotice && !toast.action && <Check className={clsx('w-3.5 h-3.5 shrink-0', isSuccess ? 'text-emerald-400' : 'text-green-400')} />}
|
|
212
218
|
</div>
|
|
213
219
|
{toast.detail && (
|
|
214
220
|
toast.onDetailClick ? (
|
|
@@ -292,8 +292,10 @@ export function defaultConditionTone(cond: { type?: string; status?: string }):
|
|
|
292
292
|
export function ConditionsSection({
|
|
293
293
|
conditions,
|
|
294
294
|
getConditionTone,
|
|
295
|
+
defaultExpanded,
|
|
295
296
|
}: {
|
|
296
297
|
conditions?: any[]
|
|
298
|
+
defaultExpanded?: boolean
|
|
297
299
|
getConditionTone?: (condition: any) => ConditionTone | undefined
|
|
298
300
|
}) {
|
|
299
301
|
if (!conditions || conditions.length === 0) return null
|
|
@@ -312,7 +314,7 @@ export function ConditionsSection({
|
|
|
312
314
|
return (
|
|
313
315
|
<Section
|
|
314
316
|
title={`Conditions (${conditions.length})${failCount > 0 ? ` · ${failCount} failing` : ''}`}
|
|
315
|
-
defaultExpanded={conditions.length <= 6}
|
|
317
|
+
defaultExpanded={defaultExpanded ?? conditions.length <= 6}
|
|
316
318
|
>
|
|
317
319
|
<div className="relative">
|
|
318
320
|
{/* Timeline line — sits between timestamp column and dot */}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import { Collapse } from '../ui/Collapse'
|
|
4
|
+
import type { WorkloadPodInfo } from '../../types'
|
|
5
|
+
import type { NavigateToResource } from '../../utils/navigation'
|
|
6
|
+
import { formatAge } from '../resources/resource-utils'
|
|
7
|
+
|
|
8
|
+
export function PodRow({
|
|
9
|
+
name,
|
|
10
|
+
namespace,
|
|
11
|
+
ready,
|
|
12
|
+
healthLevel,
|
|
13
|
+
detail,
|
|
14
|
+
onNavigate,
|
|
15
|
+
}: {
|
|
16
|
+
name: string
|
|
17
|
+
namespace: string
|
|
18
|
+
ready: boolean | null
|
|
19
|
+
healthLevel?: WorkloadPodInfo['healthLevel']
|
|
20
|
+
detail: string
|
|
21
|
+
onNavigate?: NavigateToResource
|
|
22
|
+
}) {
|
|
23
|
+
const statusLabel = podStatusLabel(healthLevel, ready)
|
|
24
|
+
const statusClass = podStatusClass(healthLevel, ready)
|
|
25
|
+
const content = (
|
|
26
|
+
<>
|
|
27
|
+
<span className={clsx('h-2 w-2 shrink-0 rounded-full', podDotClass(healthLevel, ready))} />
|
|
28
|
+
<span className="min-w-0 flex-1">
|
|
29
|
+
<span className="block truncate text-sm font-medium text-theme-text-primary">{name}</span>
|
|
30
|
+
<span className="block truncate text-xs text-theme-text-tertiary">{detail}</span>
|
|
31
|
+
</span>
|
|
32
|
+
<span className={clsx('badge-sm shrink-0', statusClass)}>
|
|
33
|
+
{statusLabel}
|
|
34
|
+
</span>
|
|
35
|
+
</>
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (!onNavigate) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex w-full min-w-0 items-center gap-3 rounded-md border border-theme-border bg-theme-base px-3 py-2 text-left">
|
|
41
|
+
{content}
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={() => onNavigate({ kind: 'pods', namespace, name })}
|
|
50
|
+
className="flex w-full min-w-0 items-center gap-3 rounded-md border border-theme-border bg-theme-base px-3 py-2 text-left transition-colors hover:bg-theme-hover"
|
|
51
|
+
>
|
|
52
|
+
{content}
|
|
53
|
+
</button>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function PodListFrame({
|
|
58
|
+
expanded,
|
|
59
|
+
hasOverflow,
|
|
60
|
+
overflow,
|
|
61
|
+
toggle,
|
|
62
|
+
children,
|
|
63
|
+
}: {
|
|
64
|
+
expanded: boolean
|
|
65
|
+
hasOverflow: boolean
|
|
66
|
+
overflow?: ReactNode
|
|
67
|
+
toggle?: ReactNode
|
|
68
|
+
children: ReactNode
|
|
69
|
+
}) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
<div className="space-y-2">
|
|
73
|
+
{children}
|
|
74
|
+
</div>
|
|
75
|
+
{hasOverflow && (
|
|
76
|
+
<Collapse open={expanded}>
|
|
77
|
+
{/* Long overflow lists scroll inside a capped box once open; the
|
|
78
|
+
cap comes off while closed so the collapse measures to zero. */}
|
|
79
|
+
<div className={clsx(expanded && 'max-h-[26rem] overflow-y-auto pr-1')}>
|
|
80
|
+
<div className="space-y-2">
|
|
81
|
+
{overflow}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</Collapse>
|
|
85
|
+
)}
|
|
86
|
+
{toggle}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function PodListToggle({
|
|
92
|
+
expanded,
|
|
93
|
+
hiddenCount,
|
|
94
|
+
label,
|
|
95
|
+
onToggle,
|
|
96
|
+
}: {
|
|
97
|
+
expanded: boolean
|
|
98
|
+
hiddenCount: number
|
|
99
|
+
label: string
|
|
100
|
+
onToggle: () => void
|
|
101
|
+
}) {
|
|
102
|
+
if (!expanded && hiddenCount <= 0) return null
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="pt-1">
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
aria-expanded={expanded}
|
|
109
|
+
onClick={onToggle}
|
|
110
|
+
className="text-xs font-medium text-accent-text hover:underline"
|
|
111
|
+
>
|
|
112
|
+
{expanded ? `Collapse ${label}` : `Show ${hiddenCount} more ${label}`}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function workloadPodDetail(pod: WorkloadPodInfo): string {
|
|
119
|
+
const parts: string[] = []
|
|
120
|
+
if (pod.phase) parts.push(pod.reason ? `${pod.phase} / ${pod.reason}` : pod.phase)
|
|
121
|
+
else if (pod.reason) parts.push(pod.reason)
|
|
122
|
+
parts.push(`${pod.containers.length} container${pod.containers.length === 1 ? '' : 's'}`)
|
|
123
|
+
if ((pod.restartCount ?? 0) > 0) parts.push(`${pod.restartCount} restart${pod.restartCount === 1 ? '' : 's'}`)
|
|
124
|
+
if (pod.lastTerminationReason && !['Unknown', 'Completed'].includes(pod.lastTerminationReason)) {
|
|
125
|
+
parts.push(`last ${pod.lastTerminationReason}`)
|
|
126
|
+
}
|
|
127
|
+
if (pod.createdAt) parts.push(`${formatAge(pod.createdAt)} old`)
|
|
128
|
+
return parts.join(' · ')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function podStatusLabel(healthLevel: WorkloadPodInfo['healthLevel'] | undefined, ready: boolean | null): string {
|
|
132
|
+
if (healthLevel === 'unhealthy') return 'Unhealthy'
|
|
133
|
+
if (healthLevel === 'degraded') return 'Degraded'
|
|
134
|
+
if (healthLevel === 'neutral') return ready ? 'Ready' : 'Neutral'
|
|
135
|
+
if (healthLevel === 'unknown') return 'Unknown'
|
|
136
|
+
return ready === null ? 'Unknown' : ready ? 'Ready' : 'Not ready'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function podStatusClass(healthLevel: WorkloadPodInfo['healthLevel'] | undefined, ready: boolean | null): string {
|
|
140
|
+
if (healthLevel === 'unhealthy') return 'status-unhealthy'
|
|
141
|
+
if (healthLevel === 'degraded') return 'status-degraded'
|
|
142
|
+
if (healthLevel === 'neutral') return 'status-neutral'
|
|
143
|
+
if (healthLevel === 'unknown' || ready === null) return 'status-unknown'
|
|
144
|
+
return ready ? 'status-healthy' : 'status-degraded'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function podDotClass(healthLevel: WorkloadPodInfo['healthLevel'] | undefined, ready: boolean | null): string {
|
|
148
|
+
if (healthLevel === 'unhealthy') return 'bg-red-500'
|
|
149
|
+
if (healthLevel === 'degraded') return 'bg-amber-500'
|
|
150
|
+
if (healthLevel === 'neutral') return 'bg-skyhook-500'
|
|
151
|
+
if (healthLevel === 'unknown' || ready === null) return 'bg-theme-text-tertiary'
|
|
152
|
+
return ready ? 'bg-emerald-500' : 'bg-amber-500'
|
|
153
|
+
}
|
|
154
|
+
|