@skyhook-io/k8s-ui 1.17.0 → 1.18.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/README.md +41 -0
- package/package.json +6 -6
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +7 -3
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +142 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -81
- package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
- package/src/components/gitops/insights/insights-helpers.ts +7 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +23 -0
- package/src/components/gitops/tree/tree-helpers.ts +12 -2
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +44 -6
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +8 -2
- package/src/components/issues/diagnostic.ts +2 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +12 -1
- package/src/components/issues/issues.test.ts +75 -1
- package/src/components/issues/severity.ts +30 -2
- package/src/components/issues/types.ts +55 -4
- package/src/components/logs/LogCore.theme.test.tsx +206 -3
- package/src/components/logs/LogCore.tsx +180 -53
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +369 -0
- package/src/components/logs/LogsViewer.tsx +91 -13
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +23 -31
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
- package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogBuffer.ts +48 -18
- package/src/components/logs/useLogSearch.ts +3 -1
- package/src/components/logs/useLogStream.ts +17 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ColumnPickerList.test.tsx +46 -0
- package/src/components/resources/ColumnPickerList.tsx +174 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +296 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +392 -181
- package/src/components/resources/column-order.test.ts +104 -0
- package/src/components/resources/column-order.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
- package/src/components/resources/renderers/HPARenderer.tsx +56 -16
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -40
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -20
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils.ts +96 -7
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +31 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
- package/src/components/shared/DrainPlanDialog.tsx +6 -2
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +14 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/DiffViewer.test.tsx +69 -0
- package/src/components/timeline/DiffViewer.tsx +113 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/shared.test.tsx +48 -0
- package/src/components/timeline/shared.tsx +14 -19
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +3 -71
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/Toast.tsx +15 -4
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +110 -1
- package/src/components/ui/drawer-components.tsx +62 -9
- package/src/components/ui/index.ts +4 -2
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +193 -71
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +11 -2
- package/src/types/core.ts +53 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +20 -2
- package/src/types/gitops-tree.ts +12 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +9 -0
- package/src/utils/format.ts +4 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +57 -0
- package/src/utils/navigation.ts +86 -8
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +37 -0
- package/src/utils/resource-hierarchy.ts +15 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- package/src/utils/view-transition.ts +16 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useCallback, useState, useMemo, useEffect, type ReactNode } from 'react'
|
|
1
|
+
import { useRef, useCallback, useState, useMemo, useEffect, useLayoutEffect, type ReactNode } from 'react'
|
|
2
2
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
|
3
3
|
import { Play, Square, Download, FileDown, Search, X, Terminal, RotateCcw, ChevronUp, ChevronDown, ChevronRight, CaseSensitive, Regex, WrapText, Clock, Copy, Trash2, Filter, EyeOff, Highlighter, Braces, Palette, ListCollapse, Sun, Moon } from 'lucide-react'
|
|
4
4
|
import type { LogEntry, LogLevel } from './useLogBuffer'
|
|
@@ -42,6 +42,12 @@ export type ToolbarExtraRenderer =
|
|
|
42
42
|
|
|
43
43
|
interface LogCoreProps {
|
|
44
44
|
entries: LogEntry[]
|
|
45
|
+
isFollowing?: boolean
|
|
46
|
+
onFollowingChange?: (following: boolean) => void
|
|
47
|
+
pendingLineCount?: number
|
|
48
|
+
retainedLineCount?: number
|
|
49
|
+
missingLineCount?: number
|
|
50
|
+
getRetainedEntries?: () => LogEntry[]
|
|
45
51
|
/**
|
|
46
52
|
* The unfiltered buffer, when the host filters `entries` before passing them
|
|
47
53
|
* (the workload viewer's pod picker), so that export's "All" describes the
|
|
@@ -58,7 +64,8 @@ interface LogCoreProps {
|
|
|
58
64
|
onClear?: () => void
|
|
59
65
|
toolbarExtra?: ToolbarExtraRenderer
|
|
60
66
|
showPodName?: boolean
|
|
61
|
-
emptyMessage?:
|
|
67
|
+
emptyMessage?: ReactNode
|
|
68
|
+
sourceUnavailable?: boolean
|
|
62
69
|
emptyCommand?: string | null
|
|
63
70
|
errorMessage?: string | null
|
|
64
71
|
/**
|
|
@@ -135,6 +142,12 @@ const TIP_DELAY = 150
|
|
|
135
142
|
|
|
136
143
|
export function LogCore({
|
|
137
144
|
entries,
|
|
145
|
+
isFollowing: controlledFollowing,
|
|
146
|
+
onFollowingChange,
|
|
147
|
+
pendingLineCount = 0,
|
|
148
|
+
retainedLineCount,
|
|
149
|
+
missingLineCount = 0,
|
|
150
|
+
getRetainedEntries,
|
|
138
151
|
allEntries,
|
|
139
152
|
isLoading,
|
|
140
153
|
isStreaming,
|
|
@@ -146,13 +159,40 @@ export function LogCore({
|
|
|
146
159
|
toolbarExtra,
|
|
147
160
|
showPodName = false,
|
|
148
161
|
emptyMessage = 'No logs available',
|
|
162
|
+
sourceUnavailable = false,
|
|
149
163
|
emptyCommand,
|
|
150
164
|
errorMessage,
|
|
151
165
|
forceDark,
|
|
152
166
|
defaultDark = true,
|
|
153
167
|
}: LogCoreProps) {
|
|
154
168
|
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
|
155
|
-
const [
|
|
169
|
+
const [localFollowing, setLocalFollowing] = useState(true)
|
|
170
|
+
const isFollowing = controlledFollowing ?? localFollowing
|
|
171
|
+
const followingRef = useRef(isFollowing)
|
|
172
|
+
const scrollerRef = useRef<HTMLElement | null>(null)
|
|
173
|
+
const resumeAtBottom = useRef(false)
|
|
174
|
+
const scrollbarDragging = useRef(false)
|
|
175
|
+
const scrollPosition = useRef<number | null>(null)
|
|
176
|
+
const setFollowing = useCallback((following: boolean) => {
|
|
177
|
+
followingRef.current = following
|
|
178
|
+
resumeAtBottom.current = false
|
|
179
|
+
setLocalFollowing(following)
|
|
180
|
+
onFollowingChange?.(following)
|
|
181
|
+
}, [onFollowingChange])
|
|
182
|
+
const touchY = useRef<number | null>(null)
|
|
183
|
+
const pauseFollowing = useCallback(() => setFollowing(false), [setFollowing])
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const handleSelectionChange = () => {
|
|
186
|
+
const selection = document.getSelection()
|
|
187
|
+
if (!followingRef.current || !selection || selection.isCollapsed || !selection.anchorNode || !scrollerRef.current?.contains(selection.anchorNode)) return
|
|
188
|
+
pauseFollowing()
|
|
189
|
+
const anchor = selection.anchorNode
|
|
190
|
+
const element = anchor instanceof Element ? anchor : anchor.parentElement
|
|
191
|
+
element?.closest('[data-index]')?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
192
|
+
}
|
|
193
|
+
document.addEventListener('selectionchange', handleSelectionChange)
|
|
194
|
+
return () => document.removeEventListener('selectionchange', handleSelectionChange)
|
|
195
|
+
}, [pauseFollowing])
|
|
156
196
|
const themeLocked = typeof forceDark === 'boolean'
|
|
157
197
|
// Seed isDark: forceDark prop wins; else localStorage['radar-logs-dark'];
|
|
158
198
|
// else defaultDark (the host app's theme, dark if not given). See
|
|
@@ -282,7 +322,7 @@ export function LogCore({
|
|
|
282
322
|
(e: LogEntry) => association.headIdById.get(e.id) ?? e.id,
|
|
283
323
|
[association],
|
|
284
324
|
)
|
|
285
|
-
const search = useLogSearch(levelFilteredEntries, virtuosoRef, recordIdOf)
|
|
325
|
+
const search = useLogSearch(levelFilteredEntries, virtuosoRef, recordIdOf, pauseFollowing)
|
|
286
326
|
|
|
287
327
|
const displayEntries = search.isFiltering ? search.filteredEntries : levelFilteredEntries
|
|
288
328
|
const hiddenCount = levelFilteredEntries.length - displayEntries.length
|
|
@@ -327,7 +367,14 @@ export function LogCore({
|
|
|
327
367
|
// express on screen: everything else (timestamps, ANSI, level, search) has a
|
|
328
368
|
// toolbar control, and the popover's preview shows where those land.
|
|
329
369
|
const bufferEntries = allEntries ?? entries
|
|
330
|
-
const
|
|
370
|
+
const bufferLineCount = retainedLineCount ?? bufferEntries.length
|
|
371
|
+
const retainedExportEntries = useMemo(
|
|
372
|
+
() => downloadMenu.shouldRender ? getRetainedEntries?.() ?? bufferEntries : bufferEntries,
|
|
373
|
+
[downloadMenu.shouldRender, getRetainedEntries, bufferEntries],
|
|
374
|
+
)
|
|
375
|
+
const exportHasGap = exportScope === 'all' && missingLineCount > 0
|
|
376
|
+
const gapNotice = 'Some logs between your saved view and the latest logs are no longer available.'
|
|
377
|
+
const exportEntries = exportScope === 'all' ? retainedExportEntries : displayEntries
|
|
331
378
|
const exportOptions = useMemo(
|
|
332
379
|
() => ({ format: exportFormat, showTimestamps, showPodName }),
|
|
333
380
|
[exportFormat, showTimestamps, showPodName],
|
|
@@ -339,7 +386,7 @@ export function LogCore({
|
|
|
339
386
|
[downloadMenu.shouldRender, exportEntries, exportOptions],
|
|
340
387
|
)
|
|
341
388
|
// Offering a scope that resolves to the same lines is a choice about nothing.
|
|
342
|
-
const scopeIsMeaningful = displayEntries.length !==
|
|
389
|
+
const scopeIsMeaningful = displayEntries.length !== bufferLineCount
|
|
343
390
|
|
|
344
391
|
// Scope describes the filters in play at the moment of asking, not a lasting
|
|
345
392
|
// intent, so it starts over at the narrower option every time rather than
|
|
@@ -358,15 +405,16 @@ export function LogCore({
|
|
|
358
405
|
const count = exportEntries.length
|
|
359
406
|
const failed = () => showApiError('Failed to copy logs', 'The browser blocked clipboard access.')
|
|
360
407
|
copyText(serializeLogEntries(exportEntries, exportOptions).content).then(
|
|
361
|
-
ok => ok ? showApiSuccess('Copied to clipboard', `${count} log line${count === 1 ? '' : 's'} copied
|
|
408
|
+
ok => ok ? showApiSuccess('Copied to clipboard', `${count} log line${count === 1 ? '' : 's'} copied.${exportHasGap ? ` ${gapNotice}` : ''}`) : failed(),
|
|
362
409
|
failed,
|
|
363
410
|
)
|
|
364
|
-
}, [closeExportMenu, exportEntries, exportOptions])
|
|
411
|
+
}, [closeExportMenu, exportEntries, exportOptions, exportHasGap])
|
|
365
412
|
|
|
366
413
|
const handleExportDownload = useCallback(() => {
|
|
367
414
|
closeExportMenu()
|
|
368
415
|
onDownload(serializeLogEntries(exportEntries, exportOptions))
|
|
369
|
-
|
|
416
|
+
if (exportHasGap) showApiSuccess('Some logs were unavailable', gapNotice)
|
|
417
|
+
}, [closeExportMenu, onDownload, exportEntries, exportOptions, exportHasGap])
|
|
370
418
|
|
|
371
419
|
// Same close-on-outside-click for the timestamp format menu.
|
|
372
420
|
const tsMenuRef = useRef<HTMLDivElement>(null)
|
|
@@ -422,7 +470,7 @@ export function LogCore({
|
|
|
422
470
|
search.open()
|
|
423
471
|
return
|
|
424
472
|
}
|
|
425
|
-
if (e.key === 's' && !e.ctrlKey && !e.metaKey && !e.altKey && onStartStream) {
|
|
473
|
+
if (e.key === 's' && !e.ctrlKey && !e.metaKey && !e.altKey && onStartStream && (isStreaming || !sourceUnavailable)) {
|
|
426
474
|
const target = e.target as HTMLElement | null
|
|
427
475
|
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT' || target.isContentEditable)) return
|
|
428
476
|
e.preventDefault()
|
|
@@ -432,16 +480,11 @@ export function LogCore({
|
|
|
432
480
|
}
|
|
433
481
|
window.addEventListener('keydown', handleKeyDown)
|
|
434
482
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
435
|
-
}, [search.open, onStartStream, onStopStream, isStreaming])
|
|
436
|
-
|
|
437
|
-
const handleFollowOutput = useCallback((isAtBottom: boolean) => {
|
|
438
|
-
if (isAtBottom) return 'smooth' as const
|
|
439
|
-
return false as const
|
|
440
|
-
}, [])
|
|
483
|
+
}, [search.open, onStartStream, onStopStream, isStreaming, sourceUnavailable])
|
|
441
484
|
|
|
442
485
|
const handleAtBottomStateChange = useCallback((bottom: boolean) => {
|
|
443
|
-
|
|
444
|
-
}, [])
|
|
486
|
+
if (bottom && resumeAtBottom.current && pendingLineCount === 0) setFollowing(true)
|
|
487
|
+
}, [setFollowing, pendingLineCount])
|
|
445
488
|
|
|
446
489
|
const toggleWrap = useCallback(() => {
|
|
447
490
|
setWordWrap(prev => {
|
|
@@ -530,13 +573,54 @@ export function LogCore({
|
|
|
530
573
|
return groupContinuations(displayEntries, association.headIdById)
|
|
531
574
|
}, [displayEntries, collapseStacks, highlightQuery, association])
|
|
532
575
|
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
576
|
+
const pinToBottom = useCallback(() => {
|
|
577
|
+
const scroller = scrollerRef.current
|
|
578
|
+
if (!followingRef.current || !scroller || scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop <= 1) return
|
|
579
|
+
scroller.scrollTop = scroller.scrollHeight
|
|
580
|
+
}, [])
|
|
581
|
+
useLayoutEffect(() => {
|
|
582
|
+
followingRef.current = isFollowing
|
|
583
|
+
pinToBottom()
|
|
584
|
+
}, [groupedEntries, isFollowing, pinToBottom])
|
|
585
|
+
const pinFrame = useRef<number | null>(null)
|
|
586
|
+
const viewportObserver = useRef<ResizeObserver | null>(null)
|
|
587
|
+
const schedulePinToBottom = useCallback(() => {
|
|
588
|
+
if (!followingRef.current || !scrollerRef.current || pinFrame.current !== null) return
|
|
589
|
+
pinFrame.current = requestAnimationFrame(() => {
|
|
590
|
+
pinFrame.current = null
|
|
591
|
+
pinToBottom()
|
|
538
592
|
})
|
|
539
|
-
}, [
|
|
593
|
+
}, [pinToBottom])
|
|
594
|
+
const handleScrollerRef = useCallback((element: HTMLElement | Window | null) => {
|
|
595
|
+
viewportObserver.current?.disconnect()
|
|
596
|
+
const scroller = element instanceof HTMLElement ? element : null
|
|
597
|
+
scrollerRef.current = scroller
|
|
598
|
+
if (scroller && typeof ResizeObserver !== 'undefined') {
|
|
599
|
+
viewportObserver.current = new ResizeObserver(schedulePinToBottom)
|
|
600
|
+
viewportObserver.current.observe(scroller)
|
|
601
|
+
const list = scroller.querySelector('[data-testid="virtuoso-item-list"]')
|
|
602
|
+
if (list) viewportObserver.current.observe(list, { box: 'border-box' })
|
|
603
|
+
}
|
|
604
|
+
schedulePinToBottom()
|
|
605
|
+
}, [schedulePinToBottom])
|
|
606
|
+
useEffect(() => () => {
|
|
607
|
+
if (pinFrame.current !== null) cancelAnimationFrame(pinFrame.current)
|
|
608
|
+
pinFrame.current = null
|
|
609
|
+
viewportObserver.current?.disconnect()
|
|
610
|
+
}, [])
|
|
611
|
+
|
|
612
|
+
const scrollToBottom = useCallback(() => {
|
|
613
|
+
setFollowing(true)
|
|
614
|
+
}, [setFollowing])
|
|
615
|
+
|
|
616
|
+
const returnToLiveButton = !isFollowing && (
|
|
617
|
+
<Tooltip content={missingLineCount > 0 ? `${gapNotice} Jump to the live stream.` : pendingLineCount > 0 ? 'Your place is held while new logs arrive across collected sources. Jump to the live stream.' : 'Scroll to bottom'} wrapperClassName="absolute bottom-4 right-14 z-10" position="top">
|
|
618
|
+
<button onClick={scrollToBottom} aria-label="Scroll to bottom" className="px-3 py-1.5 btn-brand text-xs rounded-full shadow-lg">
|
|
619
|
+
Scroll to bottom
|
|
620
|
+
{pendingLineCount > 0 && ` · ${pendingLineCount >= 20_000 ? '20K+' : pendingLineCount.toLocaleString()} new ${pendingLineCount === 1 ? 'line' : 'lines'}`}
|
|
621
|
+
</button>
|
|
622
|
+
</Tooltip>
|
|
623
|
+
)
|
|
540
624
|
|
|
541
625
|
const toolbarExtraNode = typeof toolbarExtra === 'function'
|
|
542
626
|
? toolbarExtra({ isDark, palette })
|
|
@@ -568,15 +652,16 @@ export function LogCore({
|
|
|
568
652
|
style={{ colorScheme: isDark ? 'dark' : 'light', fontFamily: "'SF Mono', 'Cascadia Code', 'Fira Code', Menlo, Consolas, 'DejaVu Sans Mono', monospace" }}
|
|
569
653
|
>
|
|
570
654
|
{/* Toolbar */}
|
|
571
|
-
<div className={`flex items-center gap-2 px-3 py-2 border-b ${palette.border} ${palette.toolbarBg}`}>
|
|
655
|
+
<div className={`flex flex-wrap items-center gap-2 px-3 py-2 border-b ${palette.border} ${palette.toolbarBg}`}>
|
|
572
656
|
{toolbarExtraNode}
|
|
573
657
|
|
|
574
658
|
{/* Stream / Stop toggle — only shown when streaming is supported */}
|
|
575
659
|
{onStartStream && (
|
|
576
|
-
<Tooltip content={isStreaming ? 'Stop streaming' : 'Start streaming'} delay={TIP_DELAY} position="bottom">
|
|
660
|
+
<Tooltip content={isStreaming ? 'Stop streaming' : sourceUnavailable ? 'Available once an instance is running' : 'Start streaming'} delay={TIP_DELAY} position="bottom">
|
|
577
661
|
<button
|
|
578
662
|
onClick={isStreaming ? onStopStream : onStartStream}
|
|
579
|
-
|
|
663
|
+
disabled={sourceUnavailable && !isStreaming}
|
|
664
|
+
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
580
665
|
isStreaming
|
|
581
666
|
? 'bg-green-600 text-white hover:bg-green-700'
|
|
582
667
|
: `${palette.elevatedBg} ${palette.textSecondary} ${palette.hoverBg}`
|
|
@@ -592,6 +677,7 @@ export function LogCore({
|
|
|
592
677
|
<Tooltip content="Refresh logs" delay={TIP_DELAY} position="bottom">
|
|
593
678
|
<button
|
|
594
679
|
onClick={onRefresh}
|
|
680
|
+
aria-label="Refresh logs"
|
|
595
681
|
disabled={isLoading || isStreaming}
|
|
596
682
|
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded ${palette.elevatedBg} ${palette.textSecondary} ${palette.hoverBg} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
597
683
|
>
|
|
@@ -608,7 +694,7 @@ export function LogCore({
|
|
|
608
694
|
<Tooltip key={opt.level} content={`${active ? 'Hide' : 'Show'} ${opt.noun}`} delay={TIP_DELAY} position="bottom">
|
|
609
695
|
<button
|
|
610
696
|
onClick={() => toggleLevel(opt.level)}
|
|
611
|
-
className={`px-1.5 py-0.5 text-[10px] font-medium rounded border transition-colors ${
|
|
697
|
+
className={`px-1.5 py-0.5 text-[10px] font-medium rounded border whitespace-nowrap transition-colors ${
|
|
612
698
|
active
|
|
613
699
|
? getLevelActiveColor(opt.level, palette)
|
|
614
700
|
: levelChipInactive
|
|
@@ -621,7 +707,7 @@ export function LogCore({
|
|
|
621
707
|
})}
|
|
622
708
|
</div>
|
|
623
709
|
|
|
624
|
-
<div className="flex-
|
|
710
|
+
<div role="group" aria-label="Log display and utilities" className="ml-auto flex shrink-0 flex-nowrap items-center gap-2">
|
|
625
711
|
|
|
626
712
|
{/* Structured-log display mode: icon cycles compact→expanded→raw, chevron picks explicitly. */}
|
|
627
713
|
{hasStructuredEntries && (
|
|
@@ -801,14 +887,15 @@ export function LogCore({
|
|
|
801
887
|
|
|
802
888
|
{/* Export */}
|
|
803
889
|
<div className="relative flex items-center" ref={downloadMenuRef}>
|
|
804
|
-
<Tooltip content=
|
|
890
|
+
<Tooltip content={bufferLineCount === 0 ? 'No logs loaded' : 'Export logs'} delay={TIP_DELAY} position="bottom" disabled={showDownloadMenu} preserveWrapperWhenDisabled>
|
|
805
891
|
<button
|
|
806
892
|
ref={exportTriggerRef}
|
|
893
|
+
disabled={bufferLineCount === 0}
|
|
807
894
|
onClick={toggleExportMenu}
|
|
808
895
|
aria-label="Export logs"
|
|
809
896
|
aria-haspopup="dialog"
|
|
810
897
|
aria-expanded={showDownloadMenu}
|
|
811
|
-
className={iconBtnInactive}
|
|
898
|
+
className={`${iconBtnInactive} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
812
899
|
>
|
|
813
900
|
<Download className="w-4 h-4" />
|
|
814
901
|
</button>
|
|
@@ -831,16 +918,17 @@ export function LogCore({
|
|
|
831
918
|
Filtered {displayEntries.length.toLocaleString()}
|
|
832
919
|
</ExportChoice>
|
|
833
920
|
<ExportChoice active={exportScope === 'all'} onClick={() => setExportScope('all')} palette={palette}>
|
|
834
|
-
All {
|
|
921
|
+
All {bufferLineCount.toLocaleString()}
|
|
835
922
|
</ExportChoice>
|
|
836
923
|
</div>
|
|
837
924
|
) : (
|
|
838
925
|
<span className={`text-[11px] ${palette.textSecondary}`}>
|
|
839
|
-
{
|
|
926
|
+
{bufferLineCount.toLocaleString()} line{bufferLineCount === 1 ? '' : 's'}
|
|
840
927
|
</span>
|
|
841
928
|
)}
|
|
842
929
|
</div>
|
|
843
930
|
|
|
931
|
+
{exportHasGap && <p className={`text-xs ${palette.textSecondary}`}>{gapNotice}</p>}
|
|
844
932
|
<div className="flex items-center gap-3">
|
|
845
933
|
<span id="log-export-format-label" className={exportLabelCls}>Format</span>
|
|
846
934
|
<div role="radiogroup" aria-labelledby="log-export-format-label" className="flex items-center gap-1">
|
|
@@ -884,15 +972,18 @@ export function LogCore({
|
|
|
884
972
|
|
|
885
973
|
{/* Clear */}
|
|
886
974
|
{onClear && (
|
|
887
|
-
<Tooltip content=
|
|
975
|
+
<Tooltip content={bufferLineCount === 0 ? 'No logs loaded' : 'Clear logs'} delay={TIP_DELAY} position="bottom">
|
|
888
976
|
<button
|
|
889
977
|
onClick={onClear}
|
|
890
|
-
|
|
978
|
+
aria-label="Clear logs"
|
|
979
|
+
disabled={bufferLineCount === 0}
|
|
980
|
+
className={`${iconBtnInactive} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
891
981
|
>
|
|
892
982
|
<Trash2 className="w-4 h-4" />
|
|
893
983
|
</button>
|
|
894
984
|
</Tooltip>
|
|
895
985
|
)}
|
|
986
|
+
</div>
|
|
896
987
|
</div>
|
|
897
988
|
|
|
898
989
|
{/* Search bar */}
|
|
@@ -1005,9 +1096,10 @@ export function LogCore({
|
|
|
1005
1096
|
<span>{errorMessage}</span>
|
|
1006
1097
|
</div>
|
|
1007
1098
|
) : groupedEntries.length === 0 ? (
|
|
1008
|
-
<div className={`${EMPTY_STATE_CLASS} ${palette.textTertiary}`}>
|
|
1099
|
+
<div className={`${EMPTY_STATE_CLASS} relative ${palette.textTertiary}`}>
|
|
1009
1100
|
<Terminal className="w-8 h-8" />
|
|
1010
|
-
<
|
|
1101
|
+
<div>{entries.length > 0 ? `Filters hide all ${entries.length.toLocaleString()} loaded lines` : emptyMessage}</div>
|
|
1102
|
+
{returnToLiveButton}
|
|
1011
1103
|
{entries.length === 0 && emptyCommand && (
|
|
1012
1104
|
<button type="button" onClick={() => { void copyText(emptyCommand) }} className={`mt-2 inline-flex max-w-[80%] items-center gap-2 rounded border px-3 py-2 font-mono text-xs ${palette.border} ${palette.toolbarBg}`} title="Copy recovery command">
|
|
1013
1105
|
<code className="truncate">{emptyCommand}</code>
|
|
@@ -1016,12 +1108,54 @@ export function LogCore({
|
|
|
1016
1108
|
)}
|
|
1017
1109
|
</div>
|
|
1018
1110
|
) : (
|
|
1019
|
-
<div
|
|
1111
|
+
<div
|
|
1112
|
+
className="flex-1 relative min-h-0"
|
|
1113
|
+
tabIndex={0}
|
|
1114
|
+
onFocusCapture={event => {
|
|
1115
|
+
const target = event.target
|
|
1116
|
+
if (!(target instanceof HTMLElement) || !target.closest('[data-virtuoso-scroller]')) return
|
|
1117
|
+
if (!target.closest('button, a, input, select, textarea, [role="button"]')) return
|
|
1118
|
+
setFollowing(false)
|
|
1119
|
+
target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
1120
|
+
}}
|
|
1121
|
+
onWheelCapture={event => {
|
|
1122
|
+
if (event.deltaY < 0) setFollowing(false)
|
|
1123
|
+
if (event.deltaY > 0) resumeAtBottom.current = true
|
|
1124
|
+
}}
|
|
1125
|
+
onScrollCapture={event => {
|
|
1126
|
+
const scroller = event.target
|
|
1127
|
+
if (!(scroller instanceof HTMLElement) || !scroller.hasAttribute('data-virtuoso-scroller')) return
|
|
1128
|
+
const previous = scrollPosition.current
|
|
1129
|
+
if (previous !== null && scroller.scrollTop > previous && scrollbarDragging.current && !followingRef.current) resumeAtBottom.current = true
|
|
1130
|
+
scrollPosition.current = scroller.scrollTop
|
|
1131
|
+
}}
|
|
1132
|
+
onKeyDownCapture={event => {
|
|
1133
|
+
if (['ArrowUp', 'PageUp', 'Home'].includes(event.key) || (event.key === ' ' && event.shiftKey)) setFollowing(false)
|
|
1134
|
+
if (['ArrowDown', 'PageDown', 'End'].includes(event.key) || (event.key === ' ' && !event.shiftKey)) resumeAtBottom.current = true
|
|
1135
|
+
}}
|
|
1136
|
+
onTouchStartCapture={event => { touchY.current = event.touches[0]?.clientY ?? null }}
|
|
1137
|
+
onTouchMoveCapture={event => {
|
|
1138
|
+
const y = event.touches[0]?.clientY
|
|
1139
|
+
if (y !== undefined && touchY.current !== null && y > touchY.current) setFollowing(false)
|
|
1140
|
+
if (y !== undefined && touchY.current !== null && y < touchY.current) resumeAtBottom.current = true
|
|
1141
|
+
touchY.current = y ?? null
|
|
1142
|
+
}}
|
|
1143
|
+
onPointerDownCapture={event => {
|
|
1144
|
+
const scroller = event.currentTarget.querySelector('[data-virtuoso-scroller]')
|
|
1145
|
+
scrollbarDragging.current = !!scroller && event.clientX >= scroller.getBoundingClientRect().right - 20
|
|
1146
|
+
const target = event.target
|
|
1147
|
+
const interactingWithLogs = target instanceof Element && !!target.closest('[data-virtuoso-scroller]')
|
|
1148
|
+
if (scrollbarDragging.current || (interactingWithLogs && (event.button === 0 || event.button === 1))) setFollowing(false)
|
|
1149
|
+
}}
|
|
1150
|
+
onPointerUpCapture={() => { scrollbarDragging.current = false }}
|
|
1151
|
+
onPointerCancelCapture={() => { scrollbarDragging.current = false }}
|
|
1152
|
+
>
|
|
1020
1153
|
<Virtuoso
|
|
1021
1154
|
ref={virtuosoRef}
|
|
1022
1155
|
data={groupedEntries}
|
|
1023
|
-
|
|
1024
|
-
|
|
1156
|
+
scrollerRef={handleScrollerRef}
|
|
1157
|
+
computeItemKey={(_index, group) => group.head.id}
|
|
1158
|
+
totalListHeightChanged={schedulePinToBottom}
|
|
1025
1159
|
atBottomStateChange={handleAtBottomStateChange}
|
|
1026
1160
|
atBottomThreshold={50}
|
|
1027
1161
|
increaseViewportBy={200}
|
|
@@ -1049,20 +1183,13 @@ export function LogCore({
|
|
|
1049
1183
|
className="h-full font-mono text-xs"
|
|
1050
1184
|
/>
|
|
1051
1185
|
{/* Scroll-to-bottom button */}
|
|
1052
|
-
{
|
|
1053
|
-
<button
|
|
1054
|
-
onClick={scrollToBottom}
|
|
1055
|
-
className="absolute bottom-4 right-14 px-3 py-1.5 btn-brand text-xs rounded-full shadow-lg z-10"
|
|
1056
|
-
>
|
|
1057
|
-
Scroll to bottom
|
|
1058
|
-
</button>
|
|
1059
|
-
)}
|
|
1186
|
+
{returnToLiveButton}
|
|
1060
1187
|
</div>
|
|
1061
1188
|
)}
|
|
1062
1189
|
|
|
1063
1190
|
{/* Keyboard shortcut hints */}
|
|
1064
1191
|
<div className={`flex items-center gap-4 px-3 py-1 border-t ${palette.border} ${palette.toolbarBg} text-[10px] ${palette.textDisabled}`}>
|
|
1065
|
-
{onStartStream && <Shortcut keys="S" label={isStreaming ? 'Stop stream' : 'Stream'} palette={palette} />}
|
|
1192
|
+
{onStartStream && (!sourceUnavailable || isStreaming) && <Shortcut keys="S" label={isStreaming ? 'Stop stream' : 'Stream'} palette={palette} />}
|
|
1066
1193
|
<Shortcut keys="Ctrl+F" label="Search" palette={palette} />
|
|
1067
1194
|
{search.mode !== 'hide' && (
|
|
1068
1195
|
<>
|
|
@@ -1189,7 +1316,7 @@ function LogLine({
|
|
|
1189
1316
|
const highlighted = highlightSearchMatches(plain, searchQuery, searchIsRegex, searchIsCaseSensitive)
|
|
1190
1317
|
contentElement = (
|
|
1191
1318
|
<span
|
|
1192
|
-
className={`${wordWrap ? 'whitespace-pre-wrap
|
|
1319
|
+
className={`${wordWrap ? 'whitespace-pre-wrap [overflow-wrap:anywhere]' : 'whitespace-pre'} ${levelColor}`}
|
|
1193
1320
|
dangerouslySetInnerHTML={{ __html: highlighted }}
|
|
1194
1321
|
/>
|
|
1195
1322
|
)
|
|
@@ -1209,13 +1336,13 @@ function LogLine({
|
|
|
1209
1336
|
const html = ansiToHtml(entry.content)
|
|
1210
1337
|
contentElement = (
|
|
1211
1338
|
<span
|
|
1212
|
-
className={`${wordWrap ? 'whitespace-pre-wrap
|
|
1339
|
+
className={`${wordWrap ? 'whitespace-pre-wrap [overflow-wrap:anywhere]' : 'whitespace-pre'} ${levelColor}`}
|
|
1213
1340
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
1214
1341
|
/>
|
|
1215
1342
|
)
|
|
1216
1343
|
} else {
|
|
1217
1344
|
contentElement = (
|
|
1218
|
-
<span className={`${wordWrap ? 'whitespace-pre-wrap
|
|
1345
|
+
<span className={`${wordWrap ? 'whitespace-pre-wrap [overflow-wrap:anywhere]' : 'whitespace-pre'} ${levelColor}`}>
|
|
1219
1346
|
{stripAnsi(entry.content)}
|
|
1220
1347
|
</span>
|
|
1221
1348
|
)
|
|
@@ -1247,7 +1374,7 @@ function LogLine({
|
|
|
1247
1374
|
<span className="flex-1 min-w-0">{contentElement}</span>
|
|
1248
1375
|
<button
|
|
1249
1376
|
onClick={handleCopy}
|
|
1250
|
-
className={`opacity-0 group-hover:opacity-100 ml-1 p-0.5 rounded ${palette.textTertiary} ${palette.hoverText} shrink-0 transition-opacity`}
|
|
1377
|
+
className={`opacity-0 group-hover:opacity-100 focus-visible:opacity-100 ml-1 p-0.5 rounded ${palette.textTertiary} ${palette.hoverText} shrink-0 transition-opacity`}
|
|
1251
1378
|
title="Copy line"
|
|
1252
1379
|
>
|
|
1253
1380
|
<Copy className="w-3 h-3" />
|
|
@@ -50,6 +50,7 @@ interface LogRangeSelectProps {
|
|
|
50
50
|
isDark?: boolean
|
|
51
51
|
/** When true, the control is greyed out and non-interactive. */
|
|
52
52
|
disabled?: boolean
|
|
53
|
+
disabledReason?: string
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export function LogRangeSelect({
|
|
@@ -59,10 +60,11 @@ export function LogRangeSelect({
|
|
|
59
60
|
tooltip = 'How many logs to load — by line count or time range',
|
|
60
61
|
isDark = true,
|
|
61
62
|
disabled = false,
|
|
63
|
+
disabledReason = 'Stop streaming to change the log range',
|
|
62
64
|
}: LogRangeSelectProps) {
|
|
63
65
|
const palette = getLogPalette(isDark)
|
|
64
66
|
return (
|
|
65
|
-
<Tooltip content={disabled ?
|
|
67
|
+
<Tooltip content={disabled ? disabledReason : tooltip} position="bottom">
|
|
66
68
|
<select
|
|
67
69
|
value={value}
|
|
68
70
|
onChange={(e) => onChange(e.target.value)}
|