@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.
- package/package.json +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { useRef, useCallback, useState, useMemo, useEffect, type ReactNode } from 'react'
|
|
2
|
+
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
|
3
|
+
import { Play, Square, Download, Search, X, Terminal, RotateCcw, ChevronUp, ChevronDown, CaseSensitive, Regex, WrapText, Clock, Copy, Trash2, Filter } from 'lucide-react'
|
|
4
|
+
import type { LogEntry, LogLevel } from './useLogBuffer'
|
|
5
|
+
import { useLogSearch } from './useLogSearch'
|
|
6
|
+
import { JsonLogLine } from './JsonLogLine'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
8
|
+
import {
|
|
9
|
+
formatLogTimestamp,
|
|
10
|
+
getLevelColor,
|
|
11
|
+
highlightSearchMatches,
|
|
12
|
+
stripAnsi,
|
|
13
|
+
ansiToHtml,
|
|
14
|
+
} from '../../utils/log-format'
|
|
15
|
+
|
|
16
|
+
export type DownloadFormat = 'txt' | 'json' | 'csv'
|
|
17
|
+
|
|
18
|
+
interface LogCoreProps {
|
|
19
|
+
entries: LogEntry[]
|
|
20
|
+
isLoading: boolean
|
|
21
|
+
isStreaming: boolean
|
|
22
|
+
onStartStream?: () => void
|
|
23
|
+
onStopStream: () => void
|
|
24
|
+
onRefresh: () => void
|
|
25
|
+
onDownload: (format: DownloadFormat) => void
|
|
26
|
+
onClear?: () => void
|
|
27
|
+
toolbarExtra?: ReactNode
|
|
28
|
+
showPodName?: boolean
|
|
29
|
+
emptyMessage?: string
|
|
30
|
+
errorMessage?: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LEVEL_OPTIONS: { level: LogLevel; label: string; color: string; activeColor: string }[] = [
|
|
34
|
+
{ level: 'error', label: 'ERR', color: 'text-red-400', activeColor: 'bg-red-500/30 dark:bg-red-500/20 text-red-600 dark:text-red-400 border-red-500/60 dark:border-red-500/40' },
|
|
35
|
+
{ level: 'warn', label: 'WARN', color: 'text-yellow-400', activeColor: 'bg-yellow-400/30 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 border-yellow-500/60 dark:border-yellow-500/40' },
|
|
36
|
+
{ level: 'info', label: 'INFO', color: 'text-blue-400', activeColor: 'bg-blue-500/25 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 border-blue-500/60 dark:border-blue-500/40' },
|
|
37
|
+
{ level: 'debug', label: 'DBG', color: 'text-theme-text-secondary', activeColor: 'bg-theme-surface text-theme-text-secondary border-theme-border-light' },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const TIP_DELAY = 150
|
|
41
|
+
|
|
42
|
+
export function LogCore({
|
|
43
|
+
entries,
|
|
44
|
+
isLoading,
|
|
45
|
+
isStreaming,
|
|
46
|
+
onStartStream,
|
|
47
|
+
onStopStream,
|
|
48
|
+
onRefresh,
|
|
49
|
+
onDownload,
|
|
50
|
+
onClear,
|
|
51
|
+
toolbarExtra,
|
|
52
|
+
showPodName = false,
|
|
53
|
+
emptyMessage = 'No logs available',
|
|
54
|
+
errorMessage,
|
|
55
|
+
}: LogCoreProps) {
|
|
56
|
+
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
|
57
|
+
const [atBottom, setAtBottom] = useState(true)
|
|
58
|
+
const [wordWrap, setWordWrap] = useState(() => {
|
|
59
|
+
try { return localStorage.getItem('radar-logs-wrap') !== 'false' } catch { return true }
|
|
60
|
+
})
|
|
61
|
+
const [showTimestamps, setShowTimestamps] = useState(() => {
|
|
62
|
+
try { return localStorage.getItem('radar-logs-timestamps') !== 'false' } catch { return true }
|
|
63
|
+
})
|
|
64
|
+
const [enabledLevels, setEnabledLevels] = useState<Set<LogLevel>>(
|
|
65
|
+
new Set(['error', 'warn', 'info', 'debug'])
|
|
66
|
+
)
|
|
67
|
+
const [showDownloadMenu, setShowDownloadMenu] = useState(false)
|
|
68
|
+
|
|
69
|
+
// Level-filtered entries
|
|
70
|
+
// 'unknown' logs are shown when all 4 known levels are enabled (no active filtering)
|
|
71
|
+
const levelFilteredEntries = useMemo(() => {
|
|
72
|
+
const allEnabled = LEVEL_OPTIONS.every(opt => enabledLevels.has(opt.level))
|
|
73
|
+
if (allEnabled) return entries
|
|
74
|
+
return entries.filter(e => enabledLevels.has(e.level))
|
|
75
|
+
}, [entries, enabledLevels])
|
|
76
|
+
|
|
77
|
+
// Level counts for badges
|
|
78
|
+
const levelCounts = useMemo(() => {
|
|
79
|
+
const counts: Record<LogLevel, number> = { error: 0, warn: 0, info: 0, debug: 0, unknown: 0 }
|
|
80
|
+
for (const e of entries) {
|
|
81
|
+
counts[e.level]++
|
|
82
|
+
}
|
|
83
|
+
return counts
|
|
84
|
+
}, [entries])
|
|
85
|
+
|
|
86
|
+
// Search
|
|
87
|
+
const search = useLogSearch(levelFilteredEntries, virtuosoRef)
|
|
88
|
+
|
|
89
|
+
// Display entries: use search-filtered when filter mode is active
|
|
90
|
+
const displayEntries = search.isFilterMode && search.query
|
|
91
|
+
? search.filteredEntries
|
|
92
|
+
: levelFilteredEntries
|
|
93
|
+
|
|
94
|
+
// Close download menu on next click anywhere (deferred so current click doesn't trigger it)
|
|
95
|
+
const downloadMenuRef = useRef<HTMLDivElement>(null)
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!showDownloadMenu) return
|
|
98
|
+
const handleClick = (e: MouseEvent) => {
|
|
99
|
+
if (downloadMenuRef.current?.contains(e.target as Node)) return
|
|
100
|
+
setShowDownloadMenu(false)
|
|
101
|
+
}
|
|
102
|
+
window.addEventListener('click', handleClick)
|
|
103
|
+
return () => window.removeEventListener('click', handleClick)
|
|
104
|
+
}, [showDownloadMenu])
|
|
105
|
+
|
|
106
|
+
// Keyboard shortcut: Ctrl+F to open search
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
109
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
|
110
|
+
e.preventDefault()
|
|
111
|
+
search.open()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
115
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
116
|
+
}, [search.open])
|
|
117
|
+
|
|
118
|
+
const handleFollowOutput = useCallback((isAtBottom: boolean) => {
|
|
119
|
+
if (isAtBottom) return 'smooth' as const
|
|
120
|
+
return false as const
|
|
121
|
+
}, [])
|
|
122
|
+
|
|
123
|
+
const handleAtBottomStateChange = useCallback((bottom: boolean) => {
|
|
124
|
+
setAtBottom(bottom)
|
|
125
|
+
}, [])
|
|
126
|
+
|
|
127
|
+
const scrollToBottom = useCallback(() => {
|
|
128
|
+
virtuosoRef.current?.scrollToIndex({
|
|
129
|
+
index: displayEntries.length - 1,
|
|
130
|
+
align: 'end',
|
|
131
|
+
behavior: 'smooth',
|
|
132
|
+
})
|
|
133
|
+
}, [displayEntries.length])
|
|
134
|
+
|
|
135
|
+
const toggleWrap = useCallback(() => {
|
|
136
|
+
setWordWrap(prev => {
|
|
137
|
+
const next = !prev
|
|
138
|
+
try { localStorage.setItem('radar-logs-wrap', String(next)) } catch {}
|
|
139
|
+
return next
|
|
140
|
+
})
|
|
141
|
+
}, [])
|
|
142
|
+
|
|
143
|
+
const toggleTimestamps = useCallback(() => {
|
|
144
|
+
setShowTimestamps(prev => {
|
|
145
|
+
const next = !prev
|
|
146
|
+
try { localStorage.setItem('radar-logs-timestamps', String(next)) } catch {}
|
|
147
|
+
return next
|
|
148
|
+
})
|
|
149
|
+
}, [])
|
|
150
|
+
|
|
151
|
+
const toggleLevel = useCallback((level: LogLevel) => {
|
|
152
|
+
setEnabledLevels(prev => {
|
|
153
|
+
const next = new Set(prev)
|
|
154
|
+
if (next.has(level)) {
|
|
155
|
+
next.delete(level)
|
|
156
|
+
} else {
|
|
157
|
+
next.add(level)
|
|
158
|
+
}
|
|
159
|
+
return next
|
|
160
|
+
})
|
|
161
|
+
}, [])
|
|
162
|
+
|
|
163
|
+
// Highlight set for current match
|
|
164
|
+
const currentHighlightId = search.matchIndices.length > 0
|
|
165
|
+
? (search.isFilterMode
|
|
166
|
+
? search.filteredEntries[search.currentMatch]?.id
|
|
167
|
+
: levelFilteredEntries[search.matchIndices[search.currentMatch]]?.id)
|
|
168
|
+
: -1
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className="flex flex-col h-full bg-theme-base">
|
|
172
|
+
{/* Toolbar */}
|
|
173
|
+
<div className="flex items-center gap-2 px-3 py-2 border-b border-theme-border bg-theme-surface">
|
|
174
|
+
{toolbarExtra}
|
|
175
|
+
|
|
176
|
+
{/* Stream / Stop toggle — only shown when streaming is supported */}
|
|
177
|
+
{onStartStream && (
|
|
178
|
+
<Tooltip content={isStreaming ? 'Stop streaming' : 'Start streaming'} delay={TIP_DELAY} position="bottom">
|
|
179
|
+
<button
|
|
180
|
+
onClick={isStreaming ? onStopStream : onStartStream}
|
|
181
|
+
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors ${
|
|
182
|
+
isStreaming
|
|
183
|
+
? 'bg-green-600 text-white hover:bg-green-700'
|
|
184
|
+
: 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover'
|
|
185
|
+
}`}
|
|
186
|
+
>
|
|
187
|
+
{isStreaming ? <Square className="w-3 h-3" /> : <Play className="w-3 h-3" />}
|
|
188
|
+
<span className="hidden sm:inline">{isStreaming ? 'Stop' : 'Stream'}</span>
|
|
189
|
+
</button>
|
|
190
|
+
</Tooltip>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{/* Refresh */}
|
|
194
|
+
<Tooltip content="Refresh logs" delay={TIP_DELAY} position="bottom">
|
|
195
|
+
<button
|
|
196
|
+
onClick={onRefresh}
|
|
197
|
+
disabled={isLoading || isStreaming}
|
|
198
|
+
className="flex items-center gap-1.5 px-2 py-1.5 text-xs rounded bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover disabled:opacity-50 disabled:cursor-not-allowed"
|
|
199
|
+
>
|
|
200
|
+
<RotateCcw className={`w-3 h-3 ${isLoading ? 'animate-spin' : ''}`} />
|
|
201
|
+
</button>
|
|
202
|
+
</Tooltip>
|
|
203
|
+
|
|
204
|
+
{/* Level filter toggles */}
|
|
205
|
+
<div className="flex items-center gap-1 ml-1">
|
|
206
|
+
{LEVEL_OPTIONS.map(opt => {
|
|
207
|
+
const active = enabledLevels.has(opt.level)
|
|
208
|
+
const count = levelCounts[opt.level]
|
|
209
|
+
return (
|
|
210
|
+
<Tooltip key={opt.level} content={`${active ? 'Hide' : 'Show'} ${opt.label} logs`} delay={TIP_DELAY} position="bottom">
|
|
211
|
+
<button
|
|
212
|
+
onClick={() => toggleLevel(opt.level)}
|
|
213
|
+
className={`px-1.5 py-0.5 text-[10px] font-medium rounded border transition-colors ${
|
|
214
|
+
active
|
|
215
|
+
? opt.activeColor
|
|
216
|
+
: 'border-transparent text-theme-text-disabled hover:text-theme-text-tertiary'
|
|
217
|
+
}`}
|
|
218
|
+
>
|
|
219
|
+
{opt.label}{count > 0 ? ` ${count}` : ''}
|
|
220
|
+
</button>
|
|
221
|
+
</Tooltip>
|
|
222
|
+
)
|
|
223
|
+
})}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="flex-1" />
|
|
227
|
+
|
|
228
|
+
{/* Timestamp toggle */}
|
|
229
|
+
<Tooltip content={showTimestamps ? 'Hide timestamps' : 'Show timestamps'} delay={TIP_DELAY} position="bottom">
|
|
230
|
+
<button
|
|
231
|
+
onClick={toggleTimestamps}
|
|
232
|
+
className={`p-1.5 rounded transition-colors ${
|
|
233
|
+
showTimestamps ? 'bg-blue-600/50 text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
|
|
234
|
+
}`}
|
|
235
|
+
>
|
|
236
|
+
<Clock className="w-4 h-4" />
|
|
237
|
+
</button>
|
|
238
|
+
</Tooltip>
|
|
239
|
+
|
|
240
|
+
{/* Wrap toggle */}
|
|
241
|
+
<Tooltip content={wordWrap ? 'Disable word wrap' : 'Enable word wrap'} delay={TIP_DELAY} position="bottom">
|
|
242
|
+
<button
|
|
243
|
+
onClick={toggleWrap}
|
|
244
|
+
className={`p-1.5 rounded transition-colors ${
|
|
245
|
+
wordWrap ? 'bg-blue-600/50 text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
|
|
246
|
+
}`}
|
|
247
|
+
>
|
|
248
|
+
<WrapText className="w-4 h-4" />
|
|
249
|
+
</button>
|
|
250
|
+
</Tooltip>
|
|
251
|
+
|
|
252
|
+
{/* Search toggle */}
|
|
253
|
+
<Tooltip content="Search (Ctrl+F)" delay={TIP_DELAY} position="bottom">
|
|
254
|
+
<button
|
|
255
|
+
onClick={() => search.isOpen ? search.close() : search.open()}
|
|
256
|
+
className={`p-1.5 rounded transition-colors ${
|
|
257
|
+
search.isOpen ? 'bg-blue-600 text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
|
|
258
|
+
}`}
|
|
259
|
+
>
|
|
260
|
+
<Search className="w-4 h-4" />
|
|
261
|
+
</button>
|
|
262
|
+
</Tooltip>
|
|
263
|
+
|
|
264
|
+
{/* Download */}
|
|
265
|
+
<div className="relative flex items-center" ref={downloadMenuRef}>
|
|
266
|
+
<Tooltip content="Download logs" delay={TIP_DELAY} position="bottom">
|
|
267
|
+
<button
|
|
268
|
+
onClick={() => setShowDownloadMenu(prev => !prev)}
|
|
269
|
+
className="p-1.5 rounded text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated"
|
|
270
|
+
>
|
|
271
|
+
<Download className="w-4 h-4" />
|
|
272
|
+
</button>
|
|
273
|
+
</Tooltip>
|
|
274
|
+
{showDownloadMenu && (
|
|
275
|
+
<div className="absolute top-full right-0 mt-1 w-32 bg-theme-elevated border border-theme-border rounded-lg shadow-lg z-50">
|
|
276
|
+
{(['txt', 'json', 'csv'] as DownloadFormat[]).map(fmt => (
|
|
277
|
+
<button
|
|
278
|
+
key={fmt}
|
|
279
|
+
onClick={() => { onDownload(fmt); setShowDownloadMenu(false) }}
|
|
280
|
+
className="w-full text-left px-3 py-2 text-xs text-theme-text-primary hover:bg-theme-hover first:rounded-t-lg last:rounded-b-lg"
|
|
281
|
+
>
|
|
282
|
+
{fmt.toUpperCase()}
|
|
283
|
+
</button>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
{/* Clear */}
|
|
290
|
+
{onClear && (
|
|
291
|
+
<Tooltip content="Clear logs" delay={TIP_DELAY} position="bottom">
|
|
292
|
+
<button
|
|
293
|
+
onClick={onClear}
|
|
294
|
+
className="p-1.5 rounded text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated"
|
|
295
|
+
>
|
|
296
|
+
<Trash2 className="w-4 h-4" />
|
|
297
|
+
</button>
|
|
298
|
+
</Tooltip>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{/* Search bar */}
|
|
303
|
+
{search.isOpen && (
|
|
304
|
+
<div className="flex items-center gap-2 px-3 py-2 border-b border-theme-border bg-theme-surface/50">
|
|
305
|
+
<Search className="w-4 h-4 text-theme-text-secondary shrink-0" />
|
|
306
|
+
<input
|
|
307
|
+
type="text"
|
|
308
|
+
value={search.query}
|
|
309
|
+
onChange={(e) => search.setQuery(e.target.value)}
|
|
310
|
+
onKeyDown={(e) => {
|
|
311
|
+
if (e.key === 'Escape') {
|
|
312
|
+
search.close()
|
|
313
|
+
} else if (e.key === 'Enter') {
|
|
314
|
+
if (e.shiftKey) {
|
|
315
|
+
search.goToPrev()
|
|
316
|
+
} else {
|
|
317
|
+
search.goToNext()
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}}
|
|
321
|
+
placeholder="Search logs..."
|
|
322
|
+
className="flex-1 bg-transparent text-theme-text-primary text-sm placeholder-theme-text-disabled focus:outline-none min-w-0"
|
|
323
|
+
autoFocus
|
|
324
|
+
/>
|
|
325
|
+
|
|
326
|
+
{/* Regex toggle */}
|
|
327
|
+
<Tooltip content="Regex" delay={TIP_DELAY} position="bottom">
|
|
328
|
+
<button
|
|
329
|
+
onClick={search.toggleRegex}
|
|
330
|
+
className={`p-1 rounded transition-colors ${
|
|
331
|
+
search.isRegex ? 'bg-blue-600 text-theme-text-primary' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
332
|
+
}`}
|
|
333
|
+
>
|
|
334
|
+
<Regex className="w-3.5 h-3.5" />
|
|
335
|
+
</button>
|
|
336
|
+
</Tooltip>
|
|
337
|
+
|
|
338
|
+
{/* Case sensitivity toggle */}
|
|
339
|
+
<Tooltip content="Match case" delay={TIP_DELAY} position="bottom">
|
|
340
|
+
<button
|
|
341
|
+
onClick={search.toggleCaseSensitive}
|
|
342
|
+
className={`p-1 rounded transition-colors ${
|
|
343
|
+
search.isCaseSensitive ? 'bg-blue-600 text-theme-text-primary' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
344
|
+
}`}
|
|
345
|
+
>
|
|
346
|
+
<CaseSensitive className="w-3.5 h-3.5" />
|
|
347
|
+
</button>
|
|
348
|
+
</Tooltip>
|
|
349
|
+
|
|
350
|
+
{/* Filter mode toggle */}
|
|
351
|
+
<Tooltip content={search.isFilterMode ? 'Highlight mode' : 'Filter mode'} delay={TIP_DELAY} position="bottom">
|
|
352
|
+
<button
|
|
353
|
+
onClick={search.toggleFilterMode}
|
|
354
|
+
className={`p-1 rounded transition-colors ${
|
|
355
|
+
search.isFilterMode ? 'bg-blue-600 text-theme-text-primary' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
|
|
356
|
+
}`}
|
|
357
|
+
>
|
|
358
|
+
<Filter className="w-3.5 h-3.5" />
|
|
359
|
+
</button>
|
|
360
|
+
</Tooltip>
|
|
361
|
+
|
|
362
|
+
{search.query && (
|
|
363
|
+
<>
|
|
364
|
+
<span className={`text-xs whitespace-nowrap ${search.regexError ? 'text-red-400' : 'text-theme-text-tertiary'}`}>
|
|
365
|
+
{search.regexError
|
|
366
|
+
? 'Invalid regex'
|
|
367
|
+
: search.matchCount > 0
|
|
368
|
+
? `${search.currentMatch + 1} / ${search.matchCount}`
|
|
369
|
+
: '0 results'}
|
|
370
|
+
</span>
|
|
371
|
+
|
|
372
|
+
{/* Navigation arrows */}
|
|
373
|
+
<Tooltip content="Previous (Shift+Enter)" delay={TIP_DELAY} position="bottom">
|
|
374
|
+
<button
|
|
375
|
+
onClick={search.goToPrev}
|
|
376
|
+
disabled={search.matchCount === 0}
|
|
377
|
+
className="p-1 rounded text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-30"
|
|
378
|
+
>
|
|
379
|
+
<ChevronUp className="w-3.5 h-3.5" />
|
|
380
|
+
</button>
|
|
381
|
+
</Tooltip>
|
|
382
|
+
<Tooltip content="Next (Enter)" delay={TIP_DELAY} position="bottom">
|
|
383
|
+
<button
|
|
384
|
+
onClick={search.goToNext}
|
|
385
|
+
disabled={search.matchCount === 0}
|
|
386
|
+
className="p-1 rounded text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-30"
|
|
387
|
+
>
|
|
388
|
+
<ChevronDown className="w-3.5 h-3.5" />
|
|
389
|
+
</button>
|
|
390
|
+
</Tooltip>
|
|
391
|
+
|
|
392
|
+
<button
|
|
393
|
+
onClick={() => search.setQuery('')}
|
|
394
|
+
className="p-1 rounded text-theme-text-secondary hover:text-theme-text-primary"
|
|
395
|
+
>
|
|
396
|
+
<X className="w-3 h-3" />
|
|
397
|
+
</button>
|
|
398
|
+
</>
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{/* Log content */}
|
|
404
|
+
{isLoading && entries.length === 0 ? (
|
|
405
|
+
<div className="flex-1 flex items-center justify-center text-theme-text-tertiary">
|
|
406
|
+
<div className="flex items-center gap-2">
|
|
407
|
+
<RotateCcw className="w-4 h-4 animate-spin" />
|
|
408
|
+
<span>Loading logs...</span>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
) : errorMessage ? (
|
|
412
|
+
<div className="flex-1 flex flex-col items-center justify-center text-red-400 gap-2">
|
|
413
|
+
<Terminal className="w-8 h-8" />
|
|
414
|
+
<span>{errorMessage}</span>
|
|
415
|
+
</div>
|
|
416
|
+
) : displayEntries.length === 0 ? (
|
|
417
|
+
<div className="flex-1 flex flex-col items-center justify-center text-theme-text-tertiary gap-2">
|
|
418
|
+
<Terminal className="w-8 h-8" />
|
|
419
|
+
<span>{emptyMessage}</span>
|
|
420
|
+
</div>
|
|
421
|
+
) : (
|
|
422
|
+
<div className="flex-1 relative">
|
|
423
|
+
<Virtuoso
|
|
424
|
+
ref={virtuosoRef}
|
|
425
|
+
data={displayEntries}
|
|
426
|
+
followOutput={handleFollowOutput}
|
|
427
|
+
initialTopMostItemIndex={displayEntries.length - 1}
|
|
428
|
+
atBottomStateChange={handleAtBottomStateChange}
|
|
429
|
+
atBottomThreshold={50}
|
|
430
|
+
increaseViewportBy={200}
|
|
431
|
+
itemContent={(_index, entry) => (
|
|
432
|
+
<LogLine
|
|
433
|
+
entry={entry}
|
|
434
|
+
searchQuery={search.query}
|
|
435
|
+
searchIsRegex={search.isRegex}
|
|
436
|
+
searchIsCaseSensitive={search.isCaseSensitive}
|
|
437
|
+
showPodName={showPodName}
|
|
438
|
+
showTimestamp={showTimestamps}
|
|
439
|
+
isCurrentMatch={entry.id === currentHighlightId}
|
|
440
|
+
wordWrap={wordWrap}
|
|
441
|
+
/>
|
|
442
|
+
)}
|
|
443
|
+
className="h-full font-mono text-xs"
|
|
444
|
+
/>
|
|
445
|
+
{/* Scroll-to-bottom button */}
|
|
446
|
+
{!atBottom && (
|
|
447
|
+
<button
|
|
448
|
+
onClick={scrollToBottom}
|
|
449
|
+
className="absolute bottom-4 right-14 px-3 py-1.5 bg-blue-600 text-white text-xs rounded-full shadow-lg hover:bg-blue-700 z-10"
|
|
450
|
+
>
|
|
451
|
+
Scroll to bottom
|
|
452
|
+
</button>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
)}
|
|
456
|
+
|
|
457
|
+
{/* Keyboard shortcut hints */}
|
|
458
|
+
<div className="flex items-center gap-4 px-3 py-1 border-t border-theme-border bg-theme-surface text-[10px] text-theme-text-disabled">
|
|
459
|
+
<Shortcut keys="Ctrl+F" label="Search" />
|
|
460
|
+
<Shortcut keys="Enter" label="Next match" />
|
|
461
|
+
<Shortcut keys="Shift+Enter" label="Prev match" />
|
|
462
|
+
<Shortcut keys="Esc" label="Close search" />
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function Shortcut({ keys, label }: { keys: string; label: string }) {
|
|
469
|
+
return (
|
|
470
|
+
<span className="flex items-center gap-1">
|
|
471
|
+
<kbd className="px-1 py-px rounded bg-theme-elevated border border-theme-border-light font-mono">{keys}</kbd>
|
|
472
|
+
<span>{label}</span>
|
|
473
|
+
</span>
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function LogLine({
|
|
478
|
+
entry,
|
|
479
|
+
searchQuery,
|
|
480
|
+
searchIsRegex,
|
|
481
|
+
searchIsCaseSensitive,
|
|
482
|
+
showPodName,
|
|
483
|
+
showTimestamp,
|
|
484
|
+
isCurrentMatch,
|
|
485
|
+
wordWrap,
|
|
486
|
+
}: {
|
|
487
|
+
entry: LogEntry
|
|
488
|
+
searchQuery: string
|
|
489
|
+
searchIsRegex: boolean
|
|
490
|
+
searchIsCaseSensitive: boolean
|
|
491
|
+
showPodName: boolean
|
|
492
|
+
showTimestamp: boolean
|
|
493
|
+
isCurrentMatch: boolean
|
|
494
|
+
wordWrap: boolean
|
|
495
|
+
}) {
|
|
496
|
+
const levelColor = getLevelColor(entry.level)
|
|
497
|
+
|
|
498
|
+
// Determine content rendering
|
|
499
|
+
let contentElement: React.ReactNode
|
|
500
|
+
if (searchQuery) {
|
|
501
|
+
const plain = stripAnsi(entry.content)
|
|
502
|
+
const highlighted = highlightSearchMatches(plain, searchQuery, searchIsRegex, searchIsCaseSensitive)
|
|
503
|
+
contentElement = (
|
|
504
|
+
<span
|
|
505
|
+
className={`${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'} ${levelColor}`}
|
|
506
|
+
dangerouslySetInnerHTML={{ __html: highlighted }}
|
|
507
|
+
/>
|
|
508
|
+
)
|
|
509
|
+
} else if (entry.isJson) {
|
|
510
|
+
contentElement = (
|
|
511
|
+
<JsonLogLine content={entry.content} level={entry.level} wordWrap={wordWrap} />
|
|
512
|
+
)
|
|
513
|
+
} else {
|
|
514
|
+
const html = ansiToHtml(entry.content)
|
|
515
|
+
contentElement = (
|
|
516
|
+
<span
|
|
517
|
+
className={`${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'} ${levelColor}`}
|
|
518
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
519
|
+
/>
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const handleCopy = () => {
|
|
524
|
+
const raw = stripAnsi(entry.content)
|
|
525
|
+
navigator.clipboard.writeText(raw).catch(() => {})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return (
|
|
529
|
+
<div className={`flex hover:bg-theme-surface/50 group leading-5 px-2 ${isCurrentMatch ? 'bg-yellow-500/10' : ''}`}>
|
|
530
|
+
{showTimestamp && entry.timestamp && (
|
|
531
|
+
<span className="text-theme-text-tertiary select-none pr-2 whitespace-nowrap">
|
|
532
|
+
{formatLogTimestamp(entry.timestamp)}
|
|
533
|
+
</span>
|
|
534
|
+
)}
|
|
535
|
+
{showPodName && entry.pod && (
|
|
536
|
+
<span
|
|
537
|
+
className={`${entry.podColor || 'text-theme-text-primary'} select-none pr-2 whitespace-nowrap min-w-[80px] max-w-[120px] truncate`}
|
|
538
|
+
title={entry.pod}
|
|
539
|
+
>
|
|
540
|
+
[{entry.pod.split('-').slice(-2).join('-')}]
|
|
541
|
+
</span>
|
|
542
|
+
)}
|
|
543
|
+
<span className="flex-1 min-w-0">{contentElement}</span>
|
|
544
|
+
<button
|
|
545
|
+
onClick={handleCopy}
|
|
546
|
+
className="opacity-0 group-hover:opacity-100 ml-1 p-0.5 rounded text-theme-text-tertiary hover:text-theme-text-secondary shrink-0 transition-opacity"
|
|
547
|
+
title="Copy line"
|
|
548
|
+
>
|
|
549
|
+
<Copy className="w-3 h-3" />
|
|
550
|
+
</button>
|
|
551
|
+
</div>
|
|
552
|
+
)
|
|
553
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ChevronDown } from 'lucide-react'
|
|
2
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
3
|
+
|
|
4
|
+
const SELECT_CLASS =
|
|
5
|
+
'appearance-none bg-theme-elevated text-theme-text-primary text-xs rounded px-2 py-1.5 border border-theme-border-light focus:outline-none focus:ring-1 focus:ring-blue-500'
|
|
6
|
+
|
|
7
|
+
// ── ContainerSelect ───────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
interface ContainerSelectProps {
|
|
10
|
+
containers: string[]
|
|
11
|
+
value: string
|
|
12
|
+
onChange: (value: string) => void
|
|
13
|
+
/** If true, prepend an "All containers" option with value="" */
|
|
14
|
+
includeAll?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ContainerSelect({ containers, value, onChange, includeAll = false }: ContainerSelectProps) {
|
|
18
|
+
if (containers.length <= 1 && !includeAll) return null
|
|
19
|
+
return (
|
|
20
|
+
<div className="relative">
|
|
21
|
+
<select
|
|
22
|
+
value={value}
|
|
23
|
+
onChange={(e) => onChange(e.target.value)}
|
|
24
|
+
className={`${SELECT_CLASS} pr-6`}
|
|
25
|
+
>
|
|
26
|
+
{includeAll && <option value="">All containers</option>}
|
|
27
|
+
{containers.map(c => <option key={c} value={c}>{c}</option>)}
|
|
28
|
+
</select>
|
|
29
|
+
<ChevronDown className="absolute right-1.5 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-secondary pointer-events-none" />
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── LogRangeSelect ────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
interface LogRangeSelectProps {
|
|
37
|
+
value: string
|
|
38
|
+
onChange: (value: string) => void
|
|
39
|
+
/** Line count options to show. Defaults to [100, 500, 1000, 5000]. */
|
|
40
|
+
lineOptions?: number[]
|
|
41
|
+
tooltip?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function LogRangeSelect({
|
|
45
|
+
value,
|
|
46
|
+
onChange,
|
|
47
|
+
lineOptions = [100, 500, 1000, 5000],
|
|
48
|
+
tooltip = 'How many logs to load — by line count or time range',
|
|
49
|
+
}: LogRangeSelectProps) {
|
|
50
|
+
return (
|
|
51
|
+
<Tooltip content={tooltip} position="bottom">
|
|
52
|
+
<select
|
|
53
|
+
value={value}
|
|
54
|
+
onChange={(e) => onChange(e.target.value)}
|
|
55
|
+
className={`${SELECT_CLASS} pr-5`}
|
|
56
|
+
>
|
|
57
|
+
<optgroup label="Lines">
|
|
58
|
+
{lineOptions.map(n => (
|
|
59
|
+
<option key={n} value={String(n)}>
|
|
60
|
+
{n.toLocaleString()} lines
|
|
61
|
+
</option>
|
|
62
|
+
))}
|
|
63
|
+
</optgroup>
|
|
64
|
+
<optgroup label="Time">
|
|
65
|
+
<option value="since:60">Last 1 min</option>
|
|
66
|
+
<option value="since:300">Last 5 min</option>
|
|
67
|
+
<option value="since:900">Last 15 min</option>
|
|
68
|
+
<option value="since:1800">Last 30 min</option>
|
|
69
|
+
<option value="since:3600">Last 1 hour</option>
|
|
70
|
+
</optgroup>
|
|
71
|
+
</select>
|
|
72
|
+
</Tooltip>
|
|
73
|
+
)
|
|
74
|
+
}
|