@skyhook-io/k8s-ui 1.0.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.
Files changed (193) hide show
  1. package/package.json +63 -0
  2. package/src/components/dock/BottomDock.tsx +204 -0
  3. package/src/components/dock/DockContext.tsx +212 -0
  4. package/src/components/dock/TerminalTab.tsx +333 -0
  5. package/src/components/dock/index.ts +3 -0
  6. package/src/components/gitops/GitOpsActions.tsx +227 -0
  7. package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
  8. package/src/components/gitops/ManagedResourcesList.tsx +210 -0
  9. package/src/components/gitops/SyncCountdown.tsx +146 -0
  10. package/src/components/gitops/index.ts +4 -0
  11. package/src/components/logs/JsonLogLine.tsx +90 -0
  12. package/src/components/logs/LogCore.tsx +553 -0
  13. package/src/components/logs/LogToolbarSelects.tsx +74 -0
  14. package/src/components/logs/LogsViewer.tsx +146 -0
  15. package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
  16. package/src/components/logs/index.ts +13 -0
  17. package/src/components/logs/useLogBuffer.ts +138 -0
  18. package/src/components/logs/useLogSearch.ts +151 -0
  19. package/src/components/logs/useLogStream.ts +61 -0
  20. package/src/components/resources/ResourcesView.tsx +5494 -0
  21. package/src/components/resources/index.ts +17 -0
  22. package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
  23. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
  24. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
  25. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
  26. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
  27. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
  28. package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
  29. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
  30. package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
  31. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
  32. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
  33. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
  36. package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
  37. package/src/components/resources/renderers/EventRenderer.tsx +175 -0
  38. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
  39. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
  40. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
  41. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
  42. package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
  43. package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
  44. package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
  45. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
  46. package/src/components/resources/renderers/HPARenderer.tsx +164 -0
  47. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
  48. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
  49. package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
  50. package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
  51. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
  52. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
  53. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
  54. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
  55. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
  56. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
  57. package/src/components/resources/renderers/JobRenderer.tsx +103 -0
  58. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
  59. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
  60. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
  61. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
  62. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
  63. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
  64. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
  65. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
  66. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
  67. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
  68. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
  69. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
  70. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
  71. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
  73. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
  74. package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
  75. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
  76. package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
  77. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
  78. package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
  79. package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
  80. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
  81. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
  82. package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +677 -0
  84. package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
  85. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
  86. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
  87. package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
  88. package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
  89. package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
  90. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
  91. package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
  92. package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
  93. package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
  94. package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
  95. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
  96. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
  97. package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
  98. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
  99. package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
  100. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
  101. package/src/components/resources/renderers/VPARenderer.tsx +193 -0
  102. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
  103. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
  104. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
  105. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
  106. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
  107. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
  108. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
  109. package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
  110. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
  111. package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
  112. package/src/components/resources/renderers/argo-cells.tsx +118 -0
  113. package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
  114. package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
  115. package/src/components/resources/renderers/eso-cells.tsx +115 -0
  116. package/src/components/resources/renderers/flux-cells.tsx +253 -0
  117. package/src/components/resources/renderers/index.ts +105 -0
  118. package/src/components/resources/renderers/istio-cells.tsx +210 -0
  119. package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
  120. package/src/components/resources/renderers/keda-cells.tsx +94 -0
  121. package/src/components/resources/renderers/knative-cells.tsx +396 -0
  122. package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
  123. package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
  124. package/src/components/resources/renderers/traefik-cells.tsx +120 -0
  125. package/src/components/resources/renderers/trivy-cells.tsx +153 -0
  126. package/src/components/resources/renderers/trivy-shared.tsx +67 -0
  127. package/src/components/resources/renderers/velero-cells.tsx +164 -0
  128. package/src/components/resources/resource-utils-argo.ts +155 -0
  129. package/src/components/resources/resource-utils-certmanager.ts +198 -0
  130. package/src/components/resources/resource-utils-cnpg.ts +344 -0
  131. package/src/components/resources/resource-utils-eso.ts +385 -0
  132. package/src/components/resources/resource-utils-flux.ts +186 -0
  133. package/src/components/resources/resource-utils-istio.ts +372 -0
  134. package/src/components/resources/resource-utils-karpenter.ts +150 -0
  135. package/src/components/resources/resource-utils-keda.ts +203 -0
  136. package/src/components/resources/resource-utils-knative.ts +218 -0
  137. package/src/components/resources/resource-utils-kyverno.ts +224 -0
  138. package/src/components/resources/resource-utils-prometheus.ts +159 -0
  139. package/src/components/resources/resource-utils-traefik.ts +180 -0
  140. package/src/components/resources/resource-utils-trivy.ts +146 -0
  141. package/src/components/resources/resource-utils-velero.ts +293 -0
  142. package/src/components/resources/resource-utils.ts +1558 -0
  143. package/src/components/shared/EditableYamlView.tsx +302 -0
  144. package/src/components/shared/ResourceActionsBar.tsx +924 -0
  145. package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
  146. package/src/components/shared/index.ts +3 -0
  147. package/src/components/timeline/index.ts +1 -0
  148. package/src/components/timeline/shared.tsx +614 -0
  149. package/src/components/topology/GroupNode.tsx +198 -0
  150. package/src/components/topology/K8sResourceNode.tsx +440 -0
  151. package/src/components/topology/TopologyControls.tsx +62 -0
  152. package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
  153. package/src/components/topology/TopologyGraph.tsx +748 -0
  154. package/src/components/topology/TopologySearch.tsx +304 -0
  155. package/src/components/topology/index.ts +7 -0
  156. package/src/components/topology/layout.ts +734 -0
  157. package/src/components/topology/layout.worker.ts +272 -0
  158. package/src/components/topology/topology.css +129 -0
  159. package/src/components/ui/CodeViewer.tsx +451 -0
  160. package/src/components/ui/ConfirmDialog.tsx +188 -0
  161. package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
  162. package/src/components/ui/HealthRing.tsx +72 -0
  163. package/src/components/ui/MetricsChart.tsx +259 -0
  164. package/src/components/ui/ResourceBar.tsx +68 -0
  165. package/src/components/ui/Toast.tsx +225 -0
  166. package/src/components/ui/Tooltip.tsx +165 -0
  167. package/src/components/ui/YamlEditor.tsx +280 -0
  168. package/src/components/ui/drawer-components.tsx +792 -0
  169. package/src/components/ui/index.ts +10 -0
  170. package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
  171. package/src/components/workload/WorkloadView.tsx +1109 -0
  172. package/src/components/workload/index.ts +2 -0
  173. package/src/hooks/index.ts +9 -0
  174. package/src/hooks/useAnimatedUnmount.ts +30 -0
  175. package/src/hooks/useKeyboardShortcuts.tsx +336 -0
  176. package/src/hooks/useRefreshAnimation.ts +58 -0
  177. package/src/index.ts +35 -0
  178. package/src/types/core.ts +894 -0
  179. package/src/types/gitops.ts +458 -0
  180. package/src/types/index.ts +2 -0
  181. package/src/utils/animation.ts +40 -0
  182. package/src/utils/api-resources.ts +185 -0
  183. package/src/utils/badge-colors.ts +399 -0
  184. package/src/utils/context-name.ts +72 -0
  185. package/src/utils/download.ts +19 -0
  186. package/src/utils/format.ts +233 -0
  187. package/src/utils/index.ts +10 -0
  188. package/src/utils/log-format.ts +300 -0
  189. package/src/utils/navigation.ts +119 -0
  190. package/src/utils/resource-hierarchy.ts +630 -0
  191. package/src/utils/resource-icons.ts +206 -0
  192. package/src/vite-env.d.ts +1 -0
  193. package/tsconfig.json +24 -0
@@ -0,0 +1,146 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { parseLogLine, parseLogRange } from '../../utils/log-format'
3
+ import { triggerDownload } from '../../utils/download'
4
+ import { useLogBuffer } from './useLogBuffer'
5
+ import { useLogStream } from './useLogStream'
6
+ import { ContainerSelect, LogRangeSelect } from './LogToolbarSelects'
7
+ import { LogCore } from './LogCore'
8
+ import type { DownloadFormat } from './LogCore'
9
+ import { Tooltip } from '../ui/Tooltip'
10
+ import { useToast } from '../ui/Toast'
11
+
12
+ export interface LogsFetchParams {
13
+ container: string
14
+ tailLines?: number
15
+ sinceSeconds?: number
16
+ previous?: boolean
17
+ }
18
+
19
+ export interface LogsViewerProps {
20
+ namespace: string
21
+ podName: string
22
+ containers: string[]
23
+ initialContainer?: string
24
+ /** Called to fetch logs. Return value is { [containerName]: rawLogText } */
25
+ fetchLogs: (params: LogsFetchParams) => Promise<{ [container: string]: string }>
26
+ /** If provided, the stream button is enabled. Called to open an SSE connection. */
27
+ createStream?: (params: Omit<LogsFetchParams, 'previous'>) => EventSource
28
+ }
29
+
30
+ export function LogsViewer({
31
+ namespace: _namespace,
32
+ podName,
33
+ containers,
34
+ initialContainer,
35
+ fetchLogs,
36
+ createStream,
37
+ }: LogsViewerProps) {
38
+ const [selectedContainer, setSelectedContainer] = useState(initialContainer || containers[0] || '')
39
+ const [isLoading, setIsLoading] = useState(false)
40
+ const [fetchError, setFetchError] = useState<string | null>(null)
41
+ const [logRange, setLogRange] = useState('500')
42
+ const [showPrevious, setShowPrevious] = useState(false)
43
+ const { showError, showSuccess } = useToast()
44
+
45
+ const { tailLines, sinceSeconds } = parseLogRange(logRange)
46
+ const { entries, append, set, clear } = useLogBuffer()
47
+ const { isStreaming, startStreaming, stopStreaming } = useLogStream()
48
+
49
+ const loadLogs = useCallback(async () => {
50
+ if (!selectedContainer) return
51
+ setIsLoading(true)
52
+ setFetchError(null)
53
+ try {
54
+ const data = await fetchLogs({ container: selectedContainer, tailLines, sinceSeconds, previous: showPrevious })
55
+ const logText = data[selectedContainer] ?? Object.values(data)[0] ?? ''
56
+ set(logText.split('\n').filter(Boolean).map(line => {
57
+ const { timestamp, content } = parseLogLine(line)
58
+ return { timestamp, content, container: selectedContainer }
59
+ }))
60
+ } catch (err) {
61
+ console.error('Failed to fetch logs:', err)
62
+ setFetchError(err instanceof Error ? err.message : 'Failed to fetch logs')
63
+ } finally {
64
+ setIsLoading(false)
65
+ }
66
+ }, [selectedContainer, tailLines, sinceSeconds, showPrevious, fetchLogs, set])
67
+
68
+ useEffect(() => { loadLogs() }, [loadLogs])
69
+ useEffect(() => { stopStreaming() }, [selectedContainer, stopStreaming])
70
+
71
+ const handleStartStreaming = useCallback(() => {
72
+ if (!createStream) return
73
+ startStreaming(
74
+ () => createStream({ container: selectedContainer, tailLines: 100, sinceSeconds }),
75
+ {
76
+ onLog: (data: any) => append({
77
+ timestamp: data.timestamp || '',
78
+ content: data.content || '',
79
+ container: data.container || selectedContainer,
80
+ }),
81
+ },
82
+ )
83
+ }, [createStream, startStreaming, selectedContainer, sinceSeconds, append])
84
+
85
+ const downloadLogs = useCallback((format: DownloadFormat) => {
86
+ let content: string
87
+ let mime: string
88
+ const filename = `${podName}-${selectedContainer}-logs.${format}`
89
+ switch (format) {
90
+ case 'json':
91
+ content = JSON.stringify(entries.map(l => ({ timestamp: l.timestamp, content: l.content, container: l.container })), null, 2)
92
+ mime = 'application/json'
93
+ break
94
+ case 'csv':
95
+ content = 'timestamp,container,content\n' + entries.map(l =>
96
+ `${l.timestamp},${l.container},"${l.content.replace(/"/g, '""')}"`)
97
+ .join('\n')
98
+ mime = 'text/csv'
99
+ break
100
+ default:
101
+ content = entries.map(l => `${l.timestamp} ${l.content}`).join('\n')
102
+ mime = 'text/plain'
103
+ }
104
+ try {
105
+ triggerDownload(content, mime, filename)
106
+ showSuccess('Log download started', `Saving ${filename}. Check your browser or desktop Downloads location.`)
107
+ } catch (err) {
108
+ showError('Failed to download logs', err instanceof Error ? err.message : 'Unknown download error')
109
+ }
110
+ }, [entries, podName, selectedContainer, showError, showSuccess])
111
+
112
+ const toolbarExtra = (
113
+ <>
114
+ <ContainerSelect containers={containers} value={selectedContainer} onChange={setSelectedContainer} />
115
+
116
+ <Tooltip content="Show logs from the pod's previous instance (if it was restarted). Useful for troubleshooting crashed containers." position="bottom">
117
+ <label className="flex items-center gap-1.5 text-xs text-theme-text-secondary">
118
+ <input
119
+ type="checkbox"
120
+ checked={showPrevious}
121
+ onChange={(e) => setShowPrevious(e.target.checked)}
122
+ className="w-3 h-3 rounded border-theme-border-light bg-theme-elevated text-blue-500 focus:ring-blue-500 focus:ring-offset-0"
123
+ />
124
+ <span className="border-b border-dotted border-theme-text-tertiary">Previous</span>
125
+ </label>
126
+ </Tooltip>
127
+
128
+ <LogRangeSelect value={logRange} onChange={setLogRange} />
129
+ </>
130
+ )
131
+
132
+ return (
133
+ <LogCore
134
+ entries={entries}
135
+ isLoading={isLoading}
136
+ errorMessage={fetchError}
137
+ isStreaming={isStreaming}
138
+ onStartStream={createStream ? handleStartStreaming : undefined}
139
+ onStopStream={stopStreaming}
140
+ onRefresh={loadLogs}
141
+ onDownload={downloadLogs}
142
+ onClear={clear}
143
+ toolbarExtra={toolbarExtra}
144
+ />
145
+ )
146
+ }
@@ -0,0 +1,263 @@
1
+ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
2
+ import { Filter, ChevronDown } from 'lucide-react'
3
+ import { parseLogRange } from '../../utils/log-format'
4
+ import { triggerDownload } from '../../utils/download'
5
+ import { useLogBuffer } from './useLogBuffer'
6
+ import { useLogStream } from './useLogStream'
7
+ import { ContainerSelect, LogRangeSelect } from './LogToolbarSelects'
8
+ import { LogCore } from './LogCore'
9
+ import type { DownloadFormat } from './LogCore'
10
+ import type { WorkloadPodInfo } from '../../types'
11
+ import { useToast } from '../ui/Toast'
12
+
13
+ export interface WorkloadRawLog {
14
+ pod: string
15
+ container: string
16
+ timestamp: string
17
+ content: string
18
+ }
19
+
20
+ export interface WorkloadLogsFetchParams {
21
+ container?: string
22
+ tailLines?: number
23
+ sinceSeconds?: number
24
+ }
25
+
26
+ export interface WorkloadLogsResult {
27
+ pods: WorkloadPodInfo[]
28
+ logs: WorkloadRawLog[]
29
+ }
30
+
31
+ export interface WorkloadLogsViewerProps {
32
+ /** Workload name — used for the download filename */
33
+ name: string
34
+ /**
35
+ * Called to fetch workload logs. Returns the pod list and merged log lines.
36
+ * The component owns the log range / container filter controls and passes
37
+ * the current params on every fetch.
38
+ */
39
+ fetchAll: (params: WorkloadLogsFetchParams) => Promise<WorkloadLogsResult>
40
+ /**
41
+ * If provided, the stream button is enabled.
42
+ * Called to open an SSE connection for the whole workload.
43
+ */
44
+ createStream?: (params: WorkloadLogsFetchParams) => EventSource
45
+ }
46
+
47
+ const POD_COLORS = [
48
+ 'text-blue-400', 'text-green-400', 'text-yellow-400', 'text-purple-400',
49
+ 'text-pink-400', 'text-cyan-400', 'text-orange-400', 'text-lime-400',
50
+ ]
51
+
52
+ export function WorkloadLogsViewer({ name, fetchAll, createStream }: WorkloadLogsViewerProps) {
53
+ const [selectedContainer, setSelectedContainer] = useState<string>('')
54
+ const [pods, setPods] = useState<WorkloadPodInfo[]>([])
55
+ const [selectedPods, setSelectedPods] = useState<Set<string>>(new Set())
56
+ const [isLoading, setIsLoading] = useState(false)
57
+ const [fetchError, setFetchError] = useState<string | null>(null)
58
+ const [showPodFilter, setShowPodFilter] = useState(false)
59
+ const [logRange, setLogRange] = useState('100')
60
+ const { showError, showSuccess } = useToast()
61
+
62
+ const { tailLines, sinceSeconds } = parseLogRange(logRange)
63
+ const { entries, append, set, clear } = useLogBuffer()
64
+ const { isStreaming, startStreaming, stopStreaming } = useLogStream()
65
+
66
+ const podColors = useMemo(() => {
67
+ const m = new Map<string, string>()
68
+ pods.forEach((pod, i) => m.set(pod.name, POD_COLORS[i % POD_COLORS.length]))
69
+ return m
70
+ }, [pods])
71
+
72
+ const podsInitialized = useRef(false)
73
+
74
+ const loadLogs = useCallback(async () => {
75
+ setIsLoading(true)
76
+ setFetchError(null)
77
+ try {
78
+ const result = await fetchAll({ container: selectedContainer || undefined, tailLines, sinceSeconds })
79
+
80
+ setPods(result.pods)
81
+
82
+ if (!podsInitialized.current && result.pods.length > 0) {
83
+ podsInitialized.current = true
84
+ setSelectedPods(new Set(result.pods.map(p => p.name)))
85
+ }
86
+
87
+ const colors = new Map<string, string>()
88
+ result.pods.forEach((pod, i) => colors.set(pod.name, POD_COLORS[i % POD_COLORS.length]))
89
+
90
+ set(result.logs.map(log => ({
91
+ timestamp: log.timestamp,
92
+ content: log.content,
93
+ container: log.container,
94
+ pod: log.pod,
95
+ podColor: colors.get(log.pod),
96
+ })))
97
+ } catch (err) {
98
+ console.error('Failed to fetch workload logs:', err)
99
+ setFetchError(err instanceof Error ? err.message : 'Failed to fetch logs')
100
+ } finally {
101
+ setIsLoading(false)
102
+ }
103
+ }, [fetchAll, selectedContainer, tailLines, sinceSeconds, set])
104
+
105
+ useEffect(() => { loadLogs() }, [loadLogs])
106
+ useEffect(() => { stopStreaming() }, [selectedContainer, stopStreaming])
107
+
108
+ const handleStartStreaming = useCallback(() => {
109
+ if (!createStream) return
110
+ startStreaming(
111
+ () => createStream({ container: selectedContainer || undefined, tailLines: 50, sinceSeconds }),
112
+ {
113
+ onConnected: (data: any) => {
114
+ if (data?.pods) {
115
+ setPods(data.pods)
116
+ if (selectedPods.size === 0) {
117
+ setSelectedPods(new Set((data.pods as WorkloadPodInfo[]).map((p: WorkloadPodInfo) => p.name)))
118
+ }
119
+ }
120
+ },
121
+ onLog: (data: any) => {
122
+ if (data?.pod && data.content !== undefined) {
123
+ append({
124
+ timestamp: data.timestamp || '',
125
+ content: data.content || '',
126
+ container: data.container || '',
127
+ pod: data.pod || '',
128
+ podColor: podColors.get(data.pod || ''),
129
+ })
130
+ }
131
+ },
132
+ },
133
+ 'Workload log stream error',
134
+ )
135
+ }, [createStream, startStreaming, selectedContainer, sinceSeconds, append, podColors, selectedPods.size])
136
+
137
+ const allContainers = useMemo(() => {
138
+ const s = new Set<string>()
139
+ pods.forEach(pod => pod.containers.forEach(c => s.add(c)))
140
+ return Array.from(s)
141
+ }, [pods])
142
+
143
+ const togglePod = useCallback((podName: string) => {
144
+ setSelectedPods(prev => {
145
+ const next = new Set(prev)
146
+ if (next.has(podName)) next.delete(podName)
147
+ else next.add(podName)
148
+ return next
149
+ })
150
+ }, [])
151
+
152
+ const toggleAllPods = useCallback(() => {
153
+ setSelectedPods(selectedPods.size === pods.length ? new Set() : new Set(pods.map(p => p.name)))
154
+ }, [selectedPods.size, pods])
155
+
156
+ const filteredEntries = useMemo(
157
+ () => entries.filter(e => !e.pod || selectedPods.has(e.pod)),
158
+ [entries, selectedPods],
159
+ )
160
+
161
+ const downloadLogs = useCallback((format: DownloadFormat) => {
162
+ let content: string
163
+ let mime: string
164
+ const filename = `${name}-logs.${format}`
165
+ switch (format) {
166
+ case 'json':
167
+ content = JSON.stringify(filteredEntries.map(l => ({
168
+ timestamp: l.timestamp, pod: l.pod, container: l.container, content: l.content,
169
+ })), null, 2)
170
+ mime = 'application/json'
171
+ break
172
+ case 'csv':
173
+ content = 'timestamp,pod,container,content\n' + filteredEntries.map(l =>
174
+ `${l.timestamp},${l.pod || ''},${l.container},"${l.content.replace(/"/g, '""')}"`)
175
+ .join('\n')
176
+ mime = 'text/csv'
177
+ break
178
+ default:
179
+ content = filteredEntries.map(l => `${l.timestamp} [${l.pod}/${l.container}] ${l.content}`).join('\n')
180
+ mime = 'text/plain'
181
+ }
182
+ try {
183
+ triggerDownload(content, mime, filename)
184
+ showSuccess('Log download started', `Saving ${filename}. Check your browser or desktop Downloads location.`)
185
+ } catch (err) {
186
+ showError('Failed to download logs', err instanceof Error ? err.message : 'Unknown download error')
187
+ }
188
+ }, [filteredEntries, name, showError, showSuccess])
189
+
190
+ const toolbarExtra = (
191
+ <>
192
+ {/* Pod filter */}
193
+ <div className="relative">
194
+ <button
195
+ onClick={() => setShowPodFilter(v => !v)}
196
+ className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors ${
197
+ showPodFilter ? 'bg-blue-600 text-theme-text-primary' : 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover'
198
+ }`}
199
+ >
200
+ <Filter className="w-3 h-3" />
201
+ <span>{selectedPods.size}/{pods.length} pods</span>
202
+ <ChevronDown className="w-3 h-3" />
203
+ </button>
204
+
205
+ {showPodFilter && (
206
+ <div className="absolute top-full left-0 mt-1 w-64 bg-theme-elevated border border-theme-border rounded-lg shadow-lg z-50 max-h-64 overflow-y-auto">
207
+ <div className="p-2 border-b border-theme-border">
208
+ <button onClick={toggleAllPods} className="text-xs text-blue-400 hover:text-blue-300">
209
+ {selectedPods.size === pods.length ? 'Deselect all' : 'Select all'}
210
+ </button>
211
+ </div>
212
+ {pods.map(pod => (
213
+ <label key={pod.name} className="flex items-center gap-2 px-3 py-2 hover:bg-theme-hover">
214
+ <input
215
+ type="checkbox"
216
+ checked={selectedPods.has(pod.name)}
217
+ onChange={() => togglePod(pod.name)}
218
+ className="w-3 h-3 rounded border-theme-border-light bg-theme-elevated text-blue-500 focus:ring-blue-500 focus:ring-offset-0"
219
+ />
220
+ <span className={`w-2 h-2 rounded-full ${podColors.get(pod.name)?.replace('text-', 'bg-')}`} />
221
+ <span className="text-xs text-theme-text-primary truncate flex-1">{pod.name}</span>
222
+ <span className={`text-xs ${pod.ready ? 'text-green-400' : 'text-yellow-400'}`}>
223
+ {pod.ready ? 'Ready' : 'Not Ready'}
224
+ </span>
225
+ </label>
226
+ ))}
227
+ </div>
228
+ )}
229
+ </div>
230
+
231
+ <ContainerSelect
232
+ containers={allContainers}
233
+ value={selectedContainer}
234
+ onChange={setSelectedContainer}
235
+ includeAll
236
+ />
237
+
238
+ <LogRangeSelect
239
+ value={logRange}
240
+ onChange={setLogRange}
241
+ lineOptions={[50, 100, 500, 1000]}
242
+ tooltip="How many logs to load per pod — by line count or time range"
243
+ />
244
+ </>
245
+ )
246
+
247
+ return (
248
+ <LogCore
249
+ entries={filteredEntries}
250
+ isLoading={isLoading}
251
+ isStreaming={isStreaming}
252
+ onStartStream={createStream ? handleStartStreaming : undefined}
253
+ onStopStream={stopStreaming}
254
+ onRefresh={loadLogs}
255
+ onDownload={downloadLogs}
256
+ onClear={clear}
257
+ toolbarExtra={toolbarExtra}
258
+ showPodName
259
+ emptyMessage={pods.length === 0 ? 'No pods found' : 'No logs available'}
260
+ errorMessage={fetchError}
261
+ />
262
+ )
263
+ }
@@ -0,0 +1,13 @@
1
+ export { LogCore } from './LogCore'
2
+ export type { DownloadFormat } from './LogCore'
3
+ export { useLogBuffer, detectLogLevel } from './useLogBuffer'
4
+ export type { LogLevel, LogEntry } from './useLogBuffer'
5
+ export { useLogSearch } from './useLogSearch'
6
+ export { useLogStream } from './useLogStream'
7
+ export type { LogStreamHandlers } from './useLogStream'
8
+ export { JsonLogLine } from './JsonLogLine'
9
+ export { ContainerSelect, LogRangeSelect } from './LogToolbarSelects'
10
+ export { LogsViewer } from './LogsViewer'
11
+ export type { LogsViewerProps, LogsFetchParams } from './LogsViewer'
12
+ export { WorkloadLogsViewer } from './WorkloadLogsViewer'
13
+ export type { WorkloadLogsViewerProps, WorkloadLogsFetchParams, WorkloadRawLog, WorkloadLogsResult } from './WorkloadLogsViewer'
@@ -0,0 +1,138 @@
1
+ import { useState, useRef, useCallback } from 'react'
2
+
3
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'unknown'
4
+
5
+ export interface LogEntry {
6
+ id: number
7
+ timestamp: string
8
+ content: string
9
+ container: string
10
+ pod?: string
11
+ podColor?: string
12
+ level: LogLevel
13
+ isJson: boolean
14
+ }
15
+
16
+ const MAX_BUFFER_SIZE = 10_000
17
+
18
+ /**
19
+ * Detect log level from content using word-boundary matching.
20
+ * For JSON logs, prefer the `level` or `severity` field.
21
+ */
22
+ export function detectLogLevel(content: string): LogLevel {
23
+ // Fast path for JSON: check level/severity field
24
+ const trimmed = content.trimStart()
25
+ if (trimmed[0] === '{') {
26
+ try {
27
+ const obj = JSON.parse(trimmed)
28
+ const rawLevel = obj.level ?? obj.severity ?? obj.lvl ?? ''
29
+ // Numeric levels (pino/bunyan): 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal
30
+ if (typeof rawLevel === 'number') {
31
+ if (rawLevel >= 50) return 'error'
32
+ if (rawLevel >= 40) return 'warn'
33
+ if (rawLevel >= 30) return 'info'
34
+ return 'debug'
35
+ }
36
+ const lvl = String(rawLevel).toLowerCase()
37
+ if (/^(error|err|fatal|panic|critical|crit)$/.test(lvl)) return 'error'
38
+ if (/^(warn|warning)$/.test(lvl)) return 'warn'
39
+ if (/^(info|information|notice)$/.test(lvl)) return 'info'
40
+ if (/^(debug|trace|verbose)$/.test(lvl)) return 'debug'
41
+ } catch {
42
+ // Not valid JSON, fall through to text matching
43
+ }
44
+ }
45
+
46
+ const lower = content.toLowerCase()
47
+ if (/\b(error|fatal|panic|critical|crit|exception)\b/.test(lower)) return 'error'
48
+ if (/\b(warn|warning)\b/.test(lower)) return 'warn'
49
+ if (/\b(debug|trace)\b/.test(lower)) return 'debug'
50
+ if (/\b(info)\b/.test(lower)) return 'info'
51
+ return 'unknown'
52
+ }
53
+
54
+ function isJsonContent(content: string): boolean {
55
+ const trimmed = content.trimStart()
56
+ return trimmed[0] === '{' && trimmed[trimmed.length - 1] === '}'
57
+ }
58
+
59
+ interface UseLogBufferReturn {
60
+ entries: LogEntry[]
61
+ append: (entry: Omit<LogEntry, 'id' | 'level' | 'isJson'>) => void
62
+ appendBatch: (entries: Omit<LogEntry, 'id' | 'level' | 'isJson'>[]) => void
63
+ set: (entries: Omit<LogEntry, 'id' | 'level' | 'isJson'>[]) => void
64
+ clear: () => void
65
+ }
66
+
67
+ export function useLogBuffer(): UseLogBufferReturn {
68
+ const [entries, setEntries] = useState<LogEntry[]>([])
69
+ const idCounter = useRef(0)
70
+ const pendingRef = useRef<Omit<LogEntry, 'id' | 'level' | 'isJson'>[]>([])
71
+ const rafRef = useRef<number | null>(null)
72
+
73
+ const enrichEntry = useCallback((raw: Omit<LogEntry, 'id' | 'level' | 'isJson'>): LogEntry => {
74
+ return {
75
+ ...raw,
76
+ id: idCounter.current++,
77
+ level: detectLogLevel(raw.content),
78
+ isJson: isJsonContent(raw.content),
79
+ }
80
+ }, [])
81
+
82
+ const flushPending = useCallback(() => {
83
+ rafRef.current = null
84
+ const batch = pendingRef.current
85
+ if (batch.length === 0) return
86
+ pendingRef.current = []
87
+
88
+ setEntries(prev => {
89
+ const enriched = batch.map(enrichEntry)
90
+ const combined = [...prev, ...enriched]
91
+ if (combined.length > MAX_BUFFER_SIZE) {
92
+ return combined.slice(combined.length - MAX_BUFFER_SIZE)
93
+ }
94
+ return combined
95
+ })
96
+ }, [enrichEntry])
97
+
98
+ const append = useCallback((entry: Omit<LogEntry, 'id' | 'level' | 'isJson'>) => {
99
+ pendingRef.current.push(entry)
100
+ if (rafRef.current === null) {
101
+ rafRef.current = requestAnimationFrame(flushPending)
102
+ }
103
+ }, [flushPending])
104
+
105
+ const appendBatch = useCallback((batch: Omit<LogEntry, 'id' | 'level' | 'isJson'>[]) => {
106
+ pendingRef.current.push(...batch)
107
+ if (rafRef.current === null) {
108
+ rafRef.current = requestAnimationFrame(flushPending)
109
+ }
110
+ }, [flushPending])
111
+
112
+ const set = useCallback((rawEntries: Omit<LogEntry, 'id' | 'level' | 'isJson'>[]) => {
113
+ // Cancel any pending RAF
114
+ if (rafRef.current !== null) {
115
+ cancelAnimationFrame(rafRef.current)
116
+ rafRef.current = null
117
+ }
118
+ pendingRef.current = []
119
+
120
+ const enriched = rawEntries.map(enrichEntry)
121
+ if (enriched.length > MAX_BUFFER_SIZE) {
122
+ setEntries(enriched.slice(enriched.length - MAX_BUFFER_SIZE))
123
+ } else {
124
+ setEntries(enriched)
125
+ }
126
+ }, [enrichEntry])
127
+
128
+ const clear = useCallback(() => {
129
+ if (rafRef.current !== null) {
130
+ cancelAnimationFrame(rafRef.current)
131
+ rafRef.current = null
132
+ }
133
+ pendingRef.current = []
134
+ setEntries([])
135
+ }, [])
136
+
137
+ return { entries, append, appendBatch, set, clear }
138
+ }