@skyhook-io/k8s-ui 1.8.9 → 1.8.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 +5 -4
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +569 -0
- package/src/components/applications/ApplicationDetail.tsx +2517 -450
- package/src/components/applications/ApplicationsList.tsx +4 -1
- package/src/components/applications/ApplicationsView.tsx +101 -18
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +81 -51
- package/src/components/checks/severity.ts +39 -45
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +14 -59
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
- package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
- package/src/components/gitops/insights/insights-helpers.ts +69 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
- package/src/components/issues/IssuesView.tsx +219 -143
- package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
- package/src/components/issues/ResourceIssuesSection.tsx +28 -128
- package/src/components/issues/index.ts +3 -2
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +146 -4
- package/src/components/issues/severity.ts +29 -32
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
- package/src/components/resources/ResourcesSidebar.tsx +70 -52
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +243 -53
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
- package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
- package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -70
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +140 -108
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/timeline/TimelineList.tsx +371 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +89 -16
- package/src/components/topology/TopologySearch.tsx +2 -2
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +5 -0
- package/src/components/ui/BoardSkeleton.tsx +47 -0
- package/src/components/ui/CardSection.tsx +117 -0
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +63 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2682 -472
- package/src/components/workload/index.ts +1 -1
- package/src/theme/variables.css +15 -1
- package/src/types/core.ts +57 -4
- package/src/utils/api-resources.test.ts +54 -0
- package/src/utils/api-resources.ts +68 -1
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +167 -1
- package/src/utils/applications.ts +1157 -393
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +990 -1
- package/src/utils/resource-hierarchy.ts +905 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
- package/tsconfig.json +2 -2
|
@@ -45,13 +45,22 @@ const RESOURCE_KINDS: {
|
|
|
45
45
|
{ kind: 'PodGroup', label: 'Pod Group', icon: getTopologyIcon('PodGroup'), color: 'text-lime-400', category: 'workloads' },
|
|
46
46
|
{ kind: 'Job', label: 'Job', icon: getTopologyIcon('Job'), color: 'text-orange-400', category: 'workloads' },
|
|
47
47
|
{ kind: 'CronJob', label: 'CronJob', icon: getTopologyIcon('CronJob'), color: 'text-orange-300', category: 'workloads' },
|
|
48
|
+
{ kind: 'Workflow', label: 'Workflow', icon: getTopologyIcon('Workflow'), color: 'text-purple-400', category: 'workloads' },
|
|
49
|
+
{ kind: 'CronWorkflow', label: 'CronWorkflow', icon: getTopologyIcon('CronWorkflow'), color: 'text-purple-300', category: 'workloads' },
|
|
48
50
|
|
|
49
51
|
// Config
|
|
50
52
|
{ kind: 'ConfigMap', label: 'ConfigMap', icon: getTopologyIcon('ConfigMap'), color: 'text-amber-400', category: 'config' },
|
|
51
53
|
{ kind: 'Secret', label: 'Secret', icon: getTopologyIcon('Secret'), color: 'text-red-400', category: 'config' },
|
|
54
|
+
{ kind: 'ServiceAccount', label: 'Service Account', icon: getTopologyIcon('ServiceAccount'), color: 'text-fuchsia-400', category: 'config' },
|
|
55
|
+
{ kind: 'SealedSecret', label: 'Sealed Secret', icon: getTopologyIcon('SealedSecret'), color: 'text-violet-400', category: 'config' },
|
|
56
|
+
{ kind: 'ServiceMonitor', label: 'Service Monitor', icon: getTopologyIcon('ServiceMonitor'), color: 'text-sky-400', category: 'config' },
|
|
57
|
+
{ kind: 'PodMonitor', label: 'Pod Monitor', icon: getTopologyIcon('PodMonitor'), color: 'text-sky-400', category: 'config' },
|
|
58
|
+
{ kind: 'WorkflowTemplate', label: 'WorkflowTemplate', icon: getTopologyIcon('WorkflowTemplate'), color: 'text-purple-300', category: 'config' },
|
|
59
|
+
{ kind: 'ClusterWorkflowTemplate', label: 'ClusterWorkflowTemplate', icon: getTopologyIcon('ClusterWorkflowTemplate'), color: 'text-purple-300', category: 'config' },
|
|
52
60
|
|
|
53
61
|
// Scaling
|
|
54
62
|
{ kind: 'HorizontalPodAutoscaler', label: 'HPA', icon: getTopologyIcon('HorizontalPodAutoscaler'), color: 'text-pink-400', category: 'scaling' },
|
|
63
|
+
{ kind: 'ScaledJob', label: 'ScaledJob', icon: getTopologyIcon('ScaledJob'), color: 'text-pink-300', category: 'scaling' },
|
|
55
64
|
|
|
56
65
|
// Knative
|
|
57
66
|
{ kind: 'KnativeService', label: 'Knative Service', icon: getTopologyIcon('KnativeService'), color: 'text-fuchsia-400', category: 'custom' },
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type NodeTypes,
|
|
15
15
|
type NodeChange,
|
|
16
16
|
type Viewport,
|
|
17
|
+
type FitViewOptions,
|
|
17
18
|
BackgroundVariant,
|
|
18
19
|
MarkerType,
|
|
19
20
|
} from '@xyflow/react'
|
|
@@ -22,6 +23,7 @@ import { toCanvas } from 'html-to-image'
|
|
|
22
23
|
|
|
23
24
|
import { AlertTriangle, ChevronsDownUp, ChevronsUpDown, Download, Info, Layers, LayoutGrid, Loader2, Maximize, Minus, Pause, Play, Plus, RotateCw, Shield } from 'lucide-react'
|
|
24
25
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
26
|
+
import { Input } from '../ui/Input'
|
|
25
27
|
import { Tooltip } from '../ui/Tooltip'
|
|
26
28
|
import { useToast } from '../ui/Toast'
|
|
27
29
|
import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
@@ -201,6 +203,10 @@ interface TopologyGraphProps {
|
|
|
201
203
|
/** Hover a node → reports its TopologyNode (null on leave). Drives the rail's
|
|
202
204
|
* reciprocal highlight. */
|
|
203
205
|
onNodeHover?: (node: TopologyNode | null) => void
|
|
206
|
+
/** Toggle a stable Deployment's normally-collapsed ReplicaSet tier. */
|
|
207
|
+
onToggleReplicaSets?: (ownerID: string) => void
|
|
208
|
+
/** Padding reserved around fit-to-view operations for overlaid UI. */
|
|
209
|
+
fitViewPadding?: FitViewOptions['padding']
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
export function TopologyGraph({
|
|
@@ -221,6 +227,8 @@ export function TopologyGraph({
|
|
|
221
227
|
focusNonce,
|
|
222
228
|
focusedOwnerId,
|
|
223
229
|
onNodeHover,
|
|
230
|
+
onToggleReplicaSets,
|
|
231
|
+
fitViewPadding = 0.15,
|
|
224
232
|
}: TopologyGraphProps) {
|
|
225
233
|
const isTrafficView = viewMode === 'traffic'
|
|
226
234
|
const [nodes, setNodes, onNodesChangeBase] = useNodesState([] as Node[])
|
|
@@ -701,6 +709,7 @@ export function TopologyGraph({
|
|
|
701
709
|
onExpand: isExpandablePodGroup ? handleExpandPodGroup : undefined,
|
|
702
710
|
onCollapse: expandedFromGroup ? handleCollapsePodGroup : undefined,
|
|
703
711
|
isExpanded: isExpandablePodGroup ? expandedPodGroups.has(node.id) : undefined,
|
|
712
|
+
onToggleReplicaSets: nodeData?.replicaSetsExpandable ? onToggleReplicaSets : undefined,
|
|
704
713
|
},
|
|
705
714
|
}
|
|
706
715
|
})
|
|
@@ -736,7 +745,7 @@ export function TopologyGraph({
|
|
|
736
745
|
// No cleanup function - we use version-based invalidation instead
|
|
737
746
|
// This prevents React's effect re-runs from canceling in-flight layouts
|
|
738
747
|
// when the actual structure hasn't changed
|
|
739
|
-
}, [workingNodes, workingEdges, structureKey, groupingMode, hideGroupHeader, collapsedGroups, groupLevels, handleSetLevel, handleCardClick, onMaximizeNamespace, isTrafficView, expandedPodGroups, handleExpandPodGroup, handleCollapsePodGroup, setNodes, setEdges, layoutRetryCount])
|
|
748
|
+
}, [workingNodes, workingEdges, structureKey, groupingMode, hideGroupHeader, collapsedGroups, groupLevels, handleSetLevel, handleCardClick, onMaximizeNamespace, isTrafficView, expandedPodGroups, handleExpandPodGroup, handleCollapsePodGroup, onToggleReplicaSets, setNodes, setEdges, layoutRetryCount])
|
|
740
749
|
|
|
741
750
|
// Handle node click
|
|
742
751
|
const handleNodeClick = useCallback(
|
|
@@ -829,6 +838,55 @@ export function TopologyGraph({
|
|
|
829
838
|
})
|
|
830
839
|
}, [focusedOwnerId, setNodes, nodes])
|
|
831
840
|
|
|
841
|
+
// Data-only topology updates must repaint existing nodes without forcing an
|
|
842
|
+
// ELK relayout. Structure hashing deliberately ignores status and metadata;
|
|
843
|
+
// keep the React Flow node payload in sync on that fast path.
|
|
844
|
+
useEffect(() => {
|
|
845
|
+
const topologyById = new Map(workingNodes.map(node => [node.id, node]))
|
|
846
|
+
setNodes(nds => {
|
|
847
|
+
let changed = false
|
|
848
|
+
const updated = nds.map(node => {
|
|
849
|
+
if (node.type === 'group') return node
|
|
850
|
+
const topologyNode = topologyById.get(node.id)
|
|
851
|
+
if (!topologyNode) return node
|
|
852
|
+
const nodeData = topologyNode.data as Record<string, unknown>
|
|
853
|
+
const pods = nodeData.pods
|
|
854
|
+
const isExpandablePodGroup = topologyNode.kind === 'PodGroup' && Array.isArray(pods) && pods.length > 0
|
|
855
|
+
const expandedFromGroup = nodeData.expandedFromGroup as string | undefined
|
|
856
|
+
const onExpand = isExpandablePodGroup ? handleExpandPodGroup : undefined
|
|
857
|
+
const onCollapse = expandedFromGroup ? handleCollapsePodGroup : undefined
|
|
858
|
+
const isExpanded = isExpandablePodGroup ? expandedPodGroups.has(node.id) : undefined
|
|
859
|
+
const toggleReplicaSets = nodeData.replicaSetsExpandable ? onToggleReplicaSets : undefined
|
|
860
|
+
if (
|
|
861
|
+
node.data?.kind === topologyNode.kind &&
|
|
862
|
+
node.data?.name === topologyNode.name &&
|
|
863
|
+
node.data?.status === topologyNode.status &&
|
|
864
|
+
node.data?.nodeData === topologyNode.data &&
|
|
865
|
+
node.data?.onExpand === onExpand &&
|
|
866
|
+
node.data?.onCollapse === onCollapse &&
|
|
867
|
+
node.data?.isExpanded === isExpanded &&
|
|
868
|
+
node.data?.onToggleReplicaSets === toggleReplicaSets
|
|
869
|
+
) return node
|
|
870
|
+
changed = true
|
|
871
|
+
return {
|
|
872
|
+
...node,
|
|
873
|
+
data: {
|
|
874
|
+
...node.data,
|
|
875
|
+
kind: topologyNode.kind,
|
|
876
|
+
name: topologyNode.name,
|
|
877
|
+
status: topologyNode.status,
|
|
878
|
+
nodeData: topologyNode.data,
|
|
879
|
+
onExpand,
|
|
880
|
+
onCollapse,
|
|
881
|
+
isExpanded,
|
|
882
|
+
onToggleReplicaSets: toggleReplicaSets,
|
|
883
|
+
},
|
|
884
|
+
}
|
|
885
|
+
})
|
|
886
|
+
return changed ? updated : nds
|
|
887
|
+
})
|
|
888
|
+
}, [workingNodes, expandedPodGroups, handleExpandPodGroup, handleCollapsePodGroup, onToggleReplicaSets, setNodes])
|
|
889
|
+
|
|
832
890
|
if (!topology) {
|
|
833
891
|
return <PaneLoader label="Loading topology…" className="absolute inset-0" />
|
|
834
892
|
}
|
|
@@ -897,10 +955,12 @@ export function TopologyGraph({
|
|
|
897
955
|
{/* Warning banner for partial topology data */}
|
|
898
956
|
{topology?.warnings && topology.warnings.length > 0 && (() => {
|
|
899
957
|
const rbacWarnings = topology.warnings.filter(w => w.includes('RBAC not granted'))
|
|
900
|
-
const
|
|
901
|
-
const
|
|
958
|
+
const viewWarnings = topology.warnings.filter(w => w.startsWith('Topology view:'))
|
|
959
|
+
const otherWarnings = topology.warnings.filter(w => !w.includes('RBAC not granted') && !w.startsWith('Topology view:'))
|
|
960
|
+
const isAllRbac = otherWarnings.length === 0 && viewWarnings.length === 0
|
|
961
|
+
const isViewLimited = otherWarnings.length === 0 && rbacWarnings.length === 0 && viewWarnings.length > 0
|
|
902
962
|
return (
|
|
903
|
-
<div className={`absolute top-2 left-2
|
|
963
|
+
<div className={`absolute top-2 z-10 ${isViewLimited ? 'left-2 max-w-xl' : 'left-2 right-2'} ${isAllRbac ? 'bg-amber-500/10 border-amber-500/20' : 'bg-amber-500/10 border-amber-500/30'} border rounded-lg p-2 backdrop-blur-sm`}>
|
|
904
964
|
<div className="flex items-start gap-2">
|
|
905
965
|
{isAllRbac ? (
|
|
906
966
|
<Shield className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
|
|
@@ -909,12 +969,14 @@ export function TopologyGraph({
|
|
|
909
969
|
)}
|
|
910
970
|
<div className="text-sm">
|
|
911
971
|
<span className="font-medium text-amber-400">
|
|
912
|
-
{isAllRbac ? 'Limited Access:' : 'Warning:'}
|
|
972
|
+
{isAllRbac ? 'Limited Access:' : isViewLimited ? 'View limited:' : 'Warning:'}
|
|
913
973
|
</span>
|
|
914
974
|
<span className="text-theme-text-secondary ml-1">
|
|
915
975
|
{isAllRbac
|
|
916
976
|
? `${pluralize(rbacWarnings.length, 'resource type')} not accessible due to RBAC restrictions.`
|
|
917
|
-
:
|
|
977
|
+
: isViewLimited
|
|
978
|
+
? 'Large retained run history is summarized to keep the graph readable.'
|
|
979
|
+
: 'Some resources failed to load. Data may be incomplete.'}
|
|
918
980
|
</span>
|
|
919
981
|
<details className="mt-1">
|
|
920
982
|
<summary className="text-xs text-amber-400/80 hover:text-amber-400">
|
|
@@ -927,7 +989,13 @@ export function TopologyGraph({
|
|
|
927
989
|
{rbacWarnings.map((w, i) => (
|
|
928
990
|
<li key={`rbac-${i}`} className="font-mono">{w}</li>
|
|
929
991
|
))}
|
|
930
|
-
{
|
|
992
|
+
{viewWarnings.length > 0 && (rbacWarnings.length > 0 || otherWarnings.length > 0) && (
|
|
993
|
+
<li className="text-amber-400/60 font-medium mt-1">View limits:</li>
|
|
994
|
+
)}
|
|
995
|
+
{viewWarnings.map((w, i) => (
|
|
996
|
+
<li key={`view-${i}`} className="font-mono">{w}</li>
|
|
997
|
+
))}
|
|
998
|
+
{otherWarnings.length > 0 && (rbacWarnings.length > 0 || viewWarnings.length > 0) && (
|
|
931
999
|
<li className="text-amber-400/60 font-medium mt-1">Other warnings:</li>
|
|
932
1000
|
)}
|
|
933
1001
|
{otherWarnings.map((w, i) => (
|
|
@@ -974,7 +1042,7 @@ export function TopologyGraph({
|
|
|
974
1042
|
onNodeMouseLeave={handleNodeMouseLeave}
|
|
975
1043
|
nodeTypes={nodeTypes}
|
|
976
1044
|
fitView
|
|
977
|
-
fitViewOptions={{ padding:
|
|
1045
|
+
fitViewOptions={{ padding: fitViewPadding }}
|
|
978
1046
|
minZoom={0.1}
|
|
979
1047
|
maxZoom={2}
|
|
980
1048
|
proOptions={{ hideAttribution: true }}
|
|
@@ -1021,6 +1089,7 @@ export function TopologyGraph({
|
|
|
1021
1089
|
paused={paused}
|
|
1022
1090
|
onTogglePause={onTogglePause}
|
|
1023
1091
|
onExportingChange={setIsExporting}
|
|
1092
|
+
fitViewPadding={fitViewPadding}
|
|
1024
1093
|
/>
|
|
1025
1094
|
</div>
|
|
1026
1095
|
{!isTrafficView && (
|
|
@@ -1057,6 +1126,7 @@ export function TopologyGraph({
|
|
|
1057
1126
|
focusNodeId={focusNodeId}
|
|
1058
1127
|
focusNonce={focusNonce}
|
|
1059
1128
|
onRequestExpandForNode={expandGroupForNode}
|
|
1129
|
+
fitViewPadding={fitViewPadding}
|
|
1060
1130
|
/>
|
|
1061
1131
|
</ReactFlow>
|
|
1062
1132
|
</ReactFlowProvider>
|
|
@@ -1240,9 +1310,8 @@ function ExportImageButton({ onExportingChange }: { onExportingChange: (v: boole
|
|
|
1240
1310
|
>
|
|
1241
1311
|
<div className="text-sm font-medium text-theme-text-primary mb-3">Export topology</div>
|
|
1242
1312
|
<label className="block text-xs text-theme-text-secondary mb-1">Filename</label>
|
|
1243
|
-
<
|
|
1313
|
+
<Input
|
|
1244
1314
|
ref={inputRef}
|
|
1245
|
-
type="text"
|
|
1246
1315
|
value={filename}
|
|
1247
1316
|
onChange={(e) => setFilename(e.target.value)}
|
|
1248
1317
|
className="w-full px-2 py-1.5 text-sm bg-theme-base border border-theme-border rounded text-theme-text-primary outline-none focus:border-blue-500 mb-3"
|
|
@@ -1340,11 +1409,13 @@ function CustomControlButtons({
|
|
|
1340
1409
|
paused,
|
|
1341
1410
|
onTogglePause,
|
|
1342
1411
|
onExportingChange,
|
|
1412
|
+
fitViewPadding,
|
|
1343
1413
|
}: {
|
|
1344
1414
|
showExportButton: boolean
|
|
1345
1415
|
paused: boolean
|
|
1346
1416
|
onTogglePause?: () => void
|
|
1347
1417
|
onExportingChange: (v: boolean) => void
|
|
1418
|
+
fitViewPadding: FitViewOptions['padding']
|
|
1348
1419
|
}) {
|
|
1349
1420
|
const { zoomIn, zoomOut, fitView } = useReactFlow()
|
|
1350
1421
|
const TIP = 100
|
|
@@ -1361,7 +1432,7 @@ function CustomControlButtons({
|
|
|
1361
1432
|
</button>
|
|
1362
1433
|
</Tooltip>
|
|
1363
1434
|
<Tooltip content="Fit view" delay={TIP} position="right">
|
|
1364
|
-
<button className="react-flow__controls-button" onClick={() => fitView({ padding:
|
|
1435
|
+
<button className="react-flow__controls-button" onClick={() => fitView({ padding: fitViewPadding, duration: 400 })}>
|
|
1365
1436
|
<Maximize className="w-3 h-3" />
|
|
1366
1437
|
</button>
|
|
1367
1438
|
</Tooltip>
|
|
@@ -1394,6 +1465,7 @@ function ViewportController({
|
|
|
1394
1465
|
focusNodeId,
|
|
1395
1466
|
focusNonce = 0,
|
|
1396
1467
|
onRequestExpandForNode,
|
|
1468
|
+
fitViewPadding,
|
|
1397
1469
|
}: {
|
|
1398
1470
|
viewMode: string
|
|
1399
1471
|
layoutRetryCount: number
|
|
@@ -1403,6 +1475,7 @@ function ViewportController({
|
|
|
1403
1475
|
focusNodeId?: string
|
|
1404
1476
|
focusNonce?: number
|
|
1405
1477
|
onRequestExpandForNode?: (nodeId: string) => void
|
|
1478
|
+
fitViewPadding: FitViewOptions['padding']
|
|
1406
1479
|
}) {
|
|
1407
1480
|
const { fitView, zoomIn, zoomOut, setViewport, getViewport, getInternalNode, setCenter } = useReactFlow()
|
|
1408
1481
|
const nodes = useNodes() // Reactive hook to watch node changes
|
|
@@ -1431,7 +1504,7 @@ function ViewportController({
|
|
|
1431
1504
|
description: 'Fit graph to screen',
|
|
1432
1505
|
category: 'Topology',
|
|
1433
1506
|
scope: 'topology',
|
|
1434
|
-
handler: () => fitView({ padding:
|
|
1507
|
+
handler: () => fitView({ padding: fitViewPadding, duration: VIEWPORT_ANIMATION_DURATION }),
|
|
1435
1508
|
},
|
|
1436
1509
|
{
|
|
1437
1510
|
id: 'topology-zoom-in',
|
|
@@ -1508,14 +1581,14 @@ function ViewportController({
|
|
|
1508
1581
|
if (nodesJustPopulated || viewModeChanged || retryRequested || fitViewRequested) {
|
|
1509
1582
|
const timeoutId = setTimeout(() => {
|
|
1510
1583
|
fitView({
|
|
1511
|
-
padding:
|
|
1584
|
+
padding: fitViewPadding,
|
|
1512
1585
|
duration: nodesJustPopulated ? 0 : VIEWPORT_ANIMATION_DURATION,
|
|
1513
1586
|
})
|
|
1514
1587
|
}, 10)
|
|
1515
1588
|
|
|
1516
1589
|
return () => clearTimeout(timeoutId)
|
|
1517
1590
|
}
|
|
1518
|
-
}, [viewMode, layoutRetryCount, fitViewCounter, nodes.length, fitView])
|
|
1591
|
+
}, [viewMode, layoutRetryCount, fitViewCounter, nodes.length, fitView, fitViewPadding])
|
|
1519
1592
|
|
|
1520
1593
|
// Pan/zoom to a single searched node. Gated on focusNonce so the same
|
|
1521
1594
|
// node can be re-focused, and so this never fires on background updates.
|
|
@@ -1563,10 +1636,10 @@ function ViewportController({
|
|
|
1563
1636
|
if (!fitAllAfterLayoutRef?.current) return
|
|
1564
1637
|
const id = setTimeout(() => {
|
|
1565
1638
|
fitAllAfterLayoutRef.current = false
|
|
1566
|
-
fitView({ padding:
|
|
1639
|
+
fitView({ padding: fitViewPadding, duration: VIEWPORT_ANIMATION_DURATION })
|
|
1567
1640
|
}, 250)
|
|
1568
1641
|
return () => clearTimeout(id)
|
|
1569
|
-
}, [nodes, fitView, fitAllAfterLayoutRef])
|
|
1642
|
+
}, [nodes, fitView, fitAllAfterLayoutRef, fitViewPadding])
|
|
1570
1643
|
|
|
1571
1644
|
return null
|
|
1572
1645
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
|
2
2
|
import { Search, X, ChevronRight } from 'lucide-react'
|
|
3
|
+
import { Input } from '../ui/Input'
|
|
3
4
|
import { getTopologyIcon } from '../../utils/resource-icons'
|
|
4
5
|
import { clsx } from 'clsx'
|
|
5
6
|
import type { TopologyNode } from '../../types'
|
|
@@ -225,9 +226,8 @@ export function TopologySearch({ nodes, onNodeSelect, onZoomToNode, allNodes, vi
|
|
|
225
226
|
{/* Search input */}
|
|
226
227
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border">
|
|
227
228
|
<Search className="w-5 h-5 text-theme-text-secondary" />
|
|
228
|
-
<
|
|
229
|
+
<Input
|
|
229
230
|
ref={inputRef}
|
|
230
|
-
type="text"
|
|
231
231
|
value={query}
|
|
232
232
|
onChange={(e) => setQuery(e.target.value)}
|
|
233
233
|
onKeyDown={handleInputKeyDown}
|
|
@@ -827,8 +827,16 @@ function computeWorkloadCards(
|
|
|
827
827
|
subtitle = (d.type as string) || 'ClusterIP'
|
|
828
828
|
} else if (primary.kind === 'CronJob') {
|
|
829
829
|
subtitle = (d.schedule as string) || ''
|
|
830
|
+
} else if (primary.kind === 'CronWorkflow') {
|
|
831
|
+
subtitle = (d.schedule as string) || ''
|
|
830
832
|
} else if (primary.kind === 'Job') {
|
|
831
833
|
subtitle = (d.phase as string) || ''
|
|
834
|
+
} else if (primary.kind === 'Workflow') {
|
|
835
|
+
subtitle = (d.progress as string) || (d.phase as string) || ''
|
|
836
|
+
} else if (primary.kind === 'WorkflowTemplate' || primary.kind === 'ClusterWorkflowTemplate') {
|
|
837
|
+
const entrypoint = d.entrypoint as string
|
|
838
|
+
const templateCount = d.templateCount as number
|
|
839
|
+
subtitle = entrypoint || (templateCount ? `${templateCount} templates` : '')
|
|
832
840
|
} else if (primary.kind === 'Application') {
|
|
833
841
|
const sync = d.syncStatus as string
|
|
834
842
|
const health = d.healthStatus as string
|
|
@@ -855,10 +863,10 @@ function computeWorkloadCards(
|
|
|
855
863
|
// Shared kind priority map — used by both pickGroupName and computeWorkloadCards
|
|
856
864
|
const KIND_PRIORITY: Record<string, number> = {
|
|
857
865
|
'Deployment': 1, 'Rollout': 1, 'StatefulSet': 2, 'DaemonSet': 3,
|
|
858
|
-
'CronJob': 4, 'Job': 5, 'Service': 6, 'Gateway': 7,
|
|
866
|
+
'CronJob': 4, 'CronWorkflow': 4, 'Job': 5, 'Workflow': 5, 'Service': 6, 'Gateway': 7,
|
|
859
867
|
'HTTPRoute': 6, 'GRPCRoute': 6, 'TCPRoute': 6, 'TLSRoute': 6, 'Ingress': 7,
|
|
860
868
|
'ReplicaSet': 8, 'Pod': 9, 'PodGroup': 9,
|
|
861
|
-
'ConfigMap': 10, 'Secret': 10, 'PersistentVolumeClaim': 10, 'HorizontalPodAutoscaler': 10,
|
|
869
|
+
'ConfigMap': 10, 'Secret': 10, 'ServiceAccount': 10, 'SealedSecret': 10, 'ServiceMonitor': 10, 'PodMonitor': 10, 'WorkflowTemplate': 10, 'ClusterWorkflowTemplate': 10, 'PersistentVolumeClaim': 10, 'HorizontalPodAutoscaler': 10,
|
|
862
870
|
'KnativeService': 1, 'KnativeConfiguration': 3, 'KnativeRevision': 4, 'KnativeRoute': 2,
|
|
863
871
|
'Broker': 2, 'Channel': 2, 'Trigger': 3, 'PingSource': 3, 'ApiServerSource': 3,
|
|
864
872
|
'ContainerSource': 3, 'SinkBinding': 3,
|
|
@@ -53,98 +53,6 @@
|
|
|
53
53
|
background-color: rgb(100 116 139); /* slate-500 */
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/* CSS-based topology icons (replaces Lucide SVGs - saves ~4000 DOM elements) */
|
|
57
|
-
.topology-icon {
|
|
58
|
-
display: inline-flex;
|
|
59
|
-
align-items: center;
|
|
60
|
-
justify-content: center;
|
|
61
|
-
width: 14px;
|
|
62
|
-
height: 14px;
|
|
63
|
-
border-radius: 3px;
|
|
64
|
-
font-size: 9px;
|
|
65
|
-
font-weight: 700;
|
|
66
|
-
color: white;
|
|
67
|
-
flex-shrink: 0;
|
|
68
|
-
background: #6b7280; /* gray-500 - default for unknown CRD kinds */
|
|
69
|
-
}
|
|
70
|
-
.topology-icon-internet { background: #3b82f6; border-radius: 50%; }
|
|
71
|
-
.topology-icon-ingress { background: #8b5cf6; }
|
|
72
|
-
.topology-icon-gateway { background: #8b5cf6; }
|
|
73
|
-
.topology-icon-httproute { background: #a78bfa; }
|
|
74
|
-
.topology-icon-grpcroute { background: #a78bfa; }
|
|
75
|
-
.topology-icon-tcproute { background: #a78bfa; }
|
|
76
|
-
.topology-icon-tlsroute { background: #a78bfa; }
|
|
77
|
-
.topology-icon-service { background: #3b82f6; border-radius: 50%; }
|
|
78
|
-
.topology-icon-deployment { background: #10b981; }
|
|
79
|
-
.topology-icon-rollout { background: #10b981; }
|
|
80
|
-
.topology-icon-daemonset { background: #14b8a6; }
|
|
81
|
-
.topology-icon-statefulset { background: #06b6d4; }
|
|
82
|
-
.topology-icon-replicaset { background: #22c55e; }
|
|
83
|
-
.topology-icon-pod { background: #84cc16; border-radius: 50%; }
|
|
84
|
-
.topology-icon-podgroup { background: #84cc16; border-radius: 50%; }
|
|
85
|
-
.topology-icon-configmap { background: #f59e0b; }
|
|
86
|
-
.topology-icon-secret { background: #ef4444; }
|
|
87
|
-
.topology-icon-horizontalpodautoscaler { background: #ec4899; }
|
|
88
|
-
.topology-icon-job { background: #a855f7; }
|
|
89
|
-
.topology-icon-cronjob { background: #a855f7; }
|
|
90
|
-
.topology-icon-persistentvolumeclaim { background: #06b6d4; }
|
|
91
|
-
.topology-icon-namespace { background: #6366f1; }
|
|
92
|
-
.topology-icon-application { background: #f97316; }
|
|
93
|
-
.topology-icon-kustomization { background: #0ea5e9; }
|
|
94
|
-
.topology-icon-helmrelease { background: #0ea5e9; }
|
|
95
|
-
.topology-icon-gitrepository { background: #14b8a6; }
|
|
96
|
-
.topology-icon-knativeservice { background: #d946ef; }
|
|
97
|
-
.topology-icon-knativeconfiguration { background: #c026d3; }
|
|
98
|
-
.topology-icon-knativerevision { background: #a855f7; }
|
|
99
|
-
.topology-icon-knativeroute { background: #d946ef; }
|
|
100
|
-
.topology-icon-broker { background: #c026d3; }
|
|
101
|
-
.topology-icon-trigger { background: #a855f7; }
|
|
102
|
-
.topology-icon-pingsource { background: #d946ef; }
|
|
103
|
-
.topology-icon-apiserversource { background: #c026d3; }
|
|
104
|
-
.topology-icon-containersource { background: #a855f7; }
|
|
105
|
-
.topology-icon-sinkbinding { background: #d946ef; }
|
|
106
|
-
.topology-icon-channel { background: #c026d3; }
|
|
107
|
-
.topology-icon-inmemorychannel { background: #c026d3; }
|
|
108
|
-
.topology-icon-subscription { background: #a855f7; }
|
|
109
|
-
.topology-icon-sequence { background: #d946ef; }
|
|
110
|
-
.topology-icon-parallel { background: #d946ef; }
|
|
111
|
-
.topology-icon-domainmapping { background: #d946ef; }
|
|
112
|
-
.topology-icon-serverlessservice { background: #a855f7; }
|
|
113
|
-
.topology-icon-node { background: #6366f1; }
|
|
114
|
-
.topology-icon-nodepool { background: #6366f1; }
|
|
115
|
-
.topology-icon-nodeclaim { background: #6366f1; }
|
|
116
|
-
.topology-icon-nodeclass { background: #6366f1; }
|
|
117
|
-
.topology-icon-httpproxy { background: #7c3aed; }
|
|
118
|
-
.topology-icon-capicluster { background: #4338ca; }
|
|
119
|
-
.topology-icon-machinedeployment { background: #5b4fcf; }
|
|
120
|
-
.topology-icon-machineset { background: #6d5fd6; }
|
|
121
|
-
.topology-icon-machine { background: #7c6fdd; }
|
|
122
|
-
.topology-icon-machinepool { background: #5b4fcf; }
|
|
123
|
-
.topology-icon-kubeadmcontrolplane { background: #4338ca; }
|
|
124
|
-
.topology-icon-clusterclass { background: #3b2fba; }
|
|
125
|
-
.topology-icon-machinehealthcheck { background: #8b7fe4; }
|
|
126
|
-
/* AWS CAPI infrastructure provider */
|
|
127
|
-
.topology-icon-awsmanagedcontrolplane { background: #d97706; }
|
|
128
|
-
.topology-icon-awsmanagedmachinepool { background: #d97706; }
|
|
129
|
-
.topology-icon-awsmachine { background: #f59e0b; }
|
|
130
|
-
.topology-icon-awsmachinetemplate { background: #f59e0b; }
|
|
131
|
-
.topology-icon-awsmanagedcluster { background: #d97706; }
|
|
132
|
-
.topology-icon-eksconfig { background: #f59e0b; }
|
|
133
|
-
.topology-icon-eksconfigtemplate { background: #f59e0b; }
|
|
134
|
-
|
|
135
|
-
/* GCP CAPI infrastructure provider */
|
|
136
|
-
.topology-icon-gcpmanagedcontrolplane { background: #0d9488; }
|
|
137
|
-
.topology-icon-gcpmanagedmachinepool { background: #0d9488; }
|
|
138
|
-
.topology-icon-gcpmachine { background: #14b8a6; }
|
|
139
|
-
.topology-icon-gcpmachinetemplate { background: #14b8a6; }
|
|
140
|
-
.topology-icon-gcpmanagedcluster { background: #0d9488; }
|
|
141
|
-
/* Azure CAPI infrastructure provider */
|
|
142
|
-
.topology-icon-azuremanagedcontrolplane { background: #2563eb; }
|
|
143
|
-
.topology-icon-azuremanagedmachinepool { background: #2563eb; }
|
|
144
|
-
.topology-icon-azuremachine { background: #3b82f6; }
|
|
145
|
-
.topology-icon-azuremachinetemplate { background: #3b82f6; }
|
|
146
|
-
.topology-icon-azuremanagedcluster { background: #2563eb; }
|
|
147
|
-
|
|
148
56
|
/* Background dots - uses CSS variable for theme awareness */
|
|
149
57
|
.react-flow__background pattern circle {
|
|
150
58
|
fill: var(--topology-bg-dots) !important;
|
|
@@ -91,6 +91,11 @@ const KIND: Record<string, string> = {
|
|
|
91
91
|
// Jobs
|
|
92
92
|
Job: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
93
93
|
CronJob: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
94
|
+
Workflow: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
95
|
+
CronWorkflow: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
96
|
+
WorkflowTemplate: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
97
|
+
ClusterWorkflowTemplate: 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40',
|
|
98
|
+
ScaledJob: 'bg-pink-100 text-pink-700 border-pink-300 dark:bg-pink-950/50 dark:text-pink-400 dark:border-pink-700/40',
|
|
94
99
|
|
|
95
100
|
// Scaling & Storage
|
|
96
101
|
HorizontalPodAutoscaler: 'bg-pink-100 text-pink-800 border-pink-300 dark:bg-pink-950/50 dark:text-pink-300 dark:border-pink-700/40',
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Shape-stable loading stand-ins for the list-board chassis (GitOps,
|
|
2
|
+
// Applications). Both mirror the loaded anatomy so data resolves in place:
|
|
3
|
+
// rows appear inside the table frame, facets inside the rail — no half-height
|
|
4
|
+
// rail dangling next to a centered spinner, no layout jump when the response
|
|
5
|
+
// lands.
|
|
6
|
+
|
|
7
|
+
export function BoardTableSkeleton({ rows = 10 }: { rows?: number }) {
|
|
8
|
+
return (
|
|
9
|
+
<div aria-live="polite" aria-label="Loading…" className="divide-y divide-theme-border-light">
|
|
10
|
+
{Array.from({ length: rows }, (_, i) => (
|
|
11
|
+
<div key={i} className="flex items-center gap-6 px-4 py-3" style={{ opacity: 1 - i * 0.09 }}>
|
|
12
|
+
<div className="min-w-0 flex-[2] space-y-2">
|
|
13
|
+
<div className="h-4 w-2/3 animate-pulse rounded bg-theme-hover" />
|
|
14
|
+
<div className="h-3 w-1/2 animate-pulse rounded bg-theme-hover" />
|
|
15
|
+
</div>
|
|
16
|
+
<div className="hidden h-4 flex-1 animate-pulse rounded bg-theme-hover md:block" />
|
|
17
|
+
<div className="h-5 w-20 animate-pulse rounded-full bg-theme-hover" />
|
|
18
|
+
<div className="h-5 w-20 animate-pulse rounded-full bg-theme-hover" />
|
|
19
|
+
<div className="hidden h-4 flex-[1.5] animate-pulse rounded bg-theme-hover lg:block" />
|
|
20
|
+
</div>
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Section stubs sized like the loaded rail so it keeps its height while
|
|
27
|
+
// counts are unknown — real facet buttons with "0" counts would be false
|
|
28
|
+
// zeros. `sections` = [title, rowCount] pairs approximating the loaded rail.
|
|
29
|
+
export function BoardRailSkeleton({ sections }: { sections: Array<[string, number]> }) {
|
|
30
|
+
return (
|
|
31
|
+
<div aria-hidden>
|
|
32
|
+
{sections.map(([title, rows]) => (
|
|
33
|
+
<div key={title} className="border-b border-theme-border-light px-3 py-3">
|
|
34
|
+
<div className="mb-2 h-3 w-24 animate-pulse rounded bg-theme-hover" />
|
|
35
|
+
<div className="space-y-1.5">
|
|
36
|
+
{Array.from({ length: rows }, (_, i) => (
|
|
37
|
+
<div key={i} className="flex items-center justify-between px-1.5 py-1">
|
|
38
|
+
<div className="h-3.5 animate-pulse rounded bg-theme-hover" style={{ width: `${52 - i * 6}%` }} />
|
|
39
|
+
<div className="h-3.5 w-6 animate-pulse rounded bg-theme-hover" />
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Fragment, type ComponentType, type ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// CARD SECTION PRIMITIVES — the shared "Sectioned + icons" vocabulary used by
|
|
5
|
+
// the Issue card (IssuesView) and the Check card (ChecksView) so both read as
|
|
6
|
+
// one product: ONE eyebrow style, an icon gutter per section, and a single body
|
|
7
|
+
// contrast floor (body text is never lighter than the secondary token, so it
|
|
8
|
+
// stays legible on the card surface). Severity is the only status color; section
|
|
9
|
+
// icons carry quiet role hues (what's-wrong = amber, fix = emerald, neutral =
|
|
10
|
+
// grey).
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
// Section role → eyebrow + icon hue. Body text always uses the secondary token
|
|
14
|
+
// (the contrast floor), regardless of tone.
|
|
15
|
+
export type CardSectionTone = 'neutral' | 'warn' | 'fix'
|
|
16
|
+
|
|
17
|
+
const SECTION_TONE_CLASS: Record<CardSectionTone, string> = {
|
|
18
|
+
// Eyebrow grey — the default, for neutral sections (Why it matters, Raw error,
|
|
19
|
+
// Affected resources, Context, Subject).
|
|
20
|
+
neutral: 'text-theme-text-tertiary',
|
|
21
|
+
// What's wrong — amber, the diagnosis beat.
|
|
22
|
+
warn: 'text-amber-700 dark:text-amber-300',
|
|
23
|
+
// Next step / How to fix — emerald, the action beat.
|
|
24
|
+
fix: 'text-emerald-700 dark:text-emerald-400',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The single eyebrow style: 11px / 600 / uppercase, with an icon in a fixed
|
|
28
|
+
// gutter so every section's body aligns under the same left edge.
|
|
29
|
+
export function CardSection({
|
|
30
|
+
icon: Icon,
|
|
31
|
+
label,
|
|
32
|
+
tone = 'neutral',
|
|
33
|
+
labelExtra,
|
|
34
|
+
children,
|
|
35
|
+
}: {
|
|
36
|
+
icon: ComponentType<{ className?: string }>
|
|
37
|
+
label: string
|
|
38
|
+
tone?: CardSectionTone
|
|
39
|
+
/** Optional trailing eyebrow text (e.g. "· retried 5×"), rendered muted. */
|
|
40
|
+
labelExtra?: ReactNode
|
|
41
|
+
children: ReactNode
|
|
42
|
+
}) {
|
|
43
|
+
const toneClass = SECTION_TONE_CLASS[tone]
|
|
44
|
+
return (
|
|
45
|
+
<section className="flex items-start gap-2.5">
|
|
46
|
+
{/* Icon sits in a fixed gutter, anchored to the eyebrow line (top-aligned
|
|
47
|
+
with a nudge to its optical center) — sections like Affected resources
|
|
48
|
+
can be dozens of rows tall, and a section-centered glyph would float
|
|
49
|
+
detached mid-list. */}
|
|
50
|
+
<Icon className={`mt-[-1px] h-[18px] w-[18px] shrink-0 ${toneClass}`} aria-hidden />
|
|
51
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
52
|
+
<h4 className={`text-[11px] font-semibold uppercase tracking-[0.06em] ${toneClass}`}>
|
|
53
|
+
{label}
|
|
54
|
+
{labelExtra ? <span className="font-medium normal-case tracking-normal text-theme-text-tertiary"> {labelExtra}</span> : null}
|
|
55
|
+
</h4>
|
|
56
|
+
{children}
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Body paragraph at the contrast floor — 14px, secondary token, never lighter.
|
|
63
|
+
export function CardBody({ children, className }: { children: ReactNode; className?: string }) {
|
|
64
|
+
return <p className={`text-sm leading-relaxed text-theme-text-secondary ${className ?? ''}`}>{children}</p>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The dark terminal block for raw error output. Intentionally dark in BOTH
|
|
68
|
+
// themes (it's a console surface, not chrome), so it draws from the dedicated
|
|
69
|
+
// --terminal-* tokens rather than the theme surface tokens.
|
|
70
|
+
export function TerminalBlock({ label, children }: { label?: ReactNode; children: ReactNode }) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="overflow-hidden rounded-lg border border-[var(--terminal-border)] bg-[var(--terminal-bg)]">
|
|
73
|
+
{label ? (
|
|
74
|
+
<div className="border-b border-[var(--terminal-divider)] px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.06em] text-[var(--terminal-label)]">
|
|
75
|
+
{label}
|
|
76
|
+
</div>
|
|
77
|
+
) : null}
|
|
78
|
+
<pre className="overflow-x-auto whitespace-pre-wrap break-words px-3 py-2.5 font-mono text-xs leading-relaxed text-[var(--terminal-text)]">
|
|
79
|
+
{children}
|
|
80
|
+
</pre>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The neutral category chip — drops the per-category hue so severity is the only
|
|
86
|
+
// status color. Self-contained (don't pair with badge-sm): 11px / 600, 2px×8px
|
|
87
|
+
// padding, no border, neutral grey.
|
|
88
|
+
export const NEUTRAL_CHIP_CLASS =
|
|
89
|
+
'inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-semibold leading-tight bg-theme-elevated text-theme-text-secondary'
|
|
90
|
+
|
|
91
|
+
// Mono kind chip — the grey tag for an all-caps resource Kind (the Issue row
|
|
92
|
+
// header + the Subject footer row). Mono and tighter than NEUTRAL_CHIP_CLASS,
|
|
93
|
+
// which carries the label/category chips.
|
|
94
|
+
export const KIND_CHIP_CLASS =
|
|
95
|
+
'shrink-0 rounded bg-theme-elevated px-1.5 py-px font-mono text-[11px] uppercase text-theme-text-secondary'
|
|
96
|
+
|
|
97
|
+
// Render a prose string, turning markdown `inline-code` spans into mono chips
|
|
98
|
+
// via the shared .inline-code class. Plain-text passthrough when the string
|
|
99
|
+
// carries no backticks, so it's a no-op for today's plain catalog copy and
|
|
100
|
+
// lights up automatically once backtick markup is added.
|
|
101
|
+
export function renderProse(text: string | undefined | null): ReactNode {
|
|
102
|
+
if (!text) return null
|
|
103
|
+
if (!text.includes('`')) return text
|
|
104
|
+
const parts = text.split(/(`[^`]+`)/g)
|
|
105
|
+
return parts.map((part, i) => {
|
|
106
|
+
// length > 2 excludes a bare "``" (which the split leaves in a plain part)
|
|
107
|
+
// so it renders as literal text, not an empty chip.
|
|
108
|
+
if (part.length > 2 && part.startsWith('`') && part.endsWith('`')) {
|
|
109
|
+
return (
|
|
110
|
+
<code key={i} className="inline-code">
|
|
111
|
+
{part.slice(1, -1)}
|
|
112
|
+
</code>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
return <Fragment key={i}>{part}</Fragment>
|
|
116
|
+
})
|
|
117
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
2
|
import { Copy, Check, ChevronUp, ChevronDown, X, Search } from 'lucide-react'
|
|
3
3
|
import { codeToHtml } from 'shiki'
|
|
4
|
+
import { Input } from './Input'
|
|
4
5
|
|
|
5
6
|
interface CodeViewerProps {
|
|
6
7
|
code: string
|
|
@@ -343,9 +344,8 @@ export function CodeViewer({
|
|
|
343
344
|
{searchOpen && (
|
|
344
345
|
<div className="absolute top-2 right-2 z-20 flex items-center gap-1 px-2 py-1.5 bg-theme-surface border border-theme-border rounded-lg shadow-lg backdrop-blur-sm">
|
|
345
346
|
<Search className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
|
|
346
|
-
<
|
|
347
|
+
<Input
|
|
347
348
|
ref={searchInputRef}
|
|
348
|
-
type="text"
|
|
349
349
|
value={searchQuery}
|
|
350
350
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
351
351
|
onKeyDown={handleSearchKeyDown}
|