Skip to content

Commit

Permalink
copy working
Browse files Browse the repository at this point in the history
  • Loading branch information
UncleGedd committed Nov 1, 2024
1 parent 3ac7976 commit 33f819d
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 8 deletions.
29 changes: 27 additions & 2 deletions ui/src/lib/components/AnsiDisplay/component.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@
},
})
// refs to rendered elements
let container: HTMLDivElement
let scrollAnchor: HTMLDivElement
let scrollTimeout: number
// Two-way binding for autoScroll
// Track log elements
let logElements: HTMLDivElement[] = []
export let autoScroll = true
let lastScrollTop = 0
// Debounce auto-scrolling, if many logs come in at once, this creates a smooth scroll UX
const debouncedScroll = () => {
if (!autoScroll) return
// if a new log arrives during the timeout, clear it and start over
// this prevents the scroll from jumping around
if (scrollTimeout) {
window.clearTimeout(scrollTimeout)
}
Expand All @@ -41,7 +48,7 @@
const isScrollingUp = target.scrollTop < lastScrollTop
const isAtBottom = Math.abs(target.scrollHeight - target.clientHeight - target.scrollTop) < 50
// Update autoScroll based on scroll behavior
// enable autoscroll if user is at the bottom, else disable it
autoScroll = !isScrollingUp && isAtBottom
lastScrollTop = target.scrollTop
}
Expand All @@ -51,6 +58,7 @@
debouncedScroll()
}
// used by parent component to add messages
export const addMessage = (message: string) => {
if (!message || !scrollAnchor?.parentElement) return
Expand All @@ -61,10 +69,27 @@
window.requestAnimationFrame(() => {
scrollAnchor.parentElement?.insertBefore(lineElement, scrollAnchor)
logElements.push(lineElement)
// Keep only last 1000 lines in memory to prevent memory leaks
if (logElements.length > 1000) {
logElements.shift()
}
debouncedScroll()
})
}
// Function to get last N lines of logs
export const getLastNLines = (n: number): string => {
const lastN = logElements.slice(-n)
return lastN
.map((el) => {
// Convert HTML back to plain text and remove ANSI codes
const text = el.textContent || ''
return text.replace(/\n$/, '') // Remove trailing newline if present
})
.join('\n')
}
onMount(() => {
return () => {
if (scrollTimeout) {
Expand Down
57 changes: 51 additions & 6 deletions ui/src/lib/components/k8s/Logs/component.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,53 @@
<!-- SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial -->

<script lang="ts">
import { onMount } from 'svelte'
import { onDestroy, onMount } from 'svelte'
import { derived, writable } from 'svelte/store'
import { fade } from 'svelte/transition'
import { AnsiDisplay, Dropdown } from '$components'
import type { Pod } from '$lib/types'
import { Copy, Download, Search, Terminal } from 'carbon-icons-svelte'
import { CheckmarkOutline, Copy, Download, Search, Terminal } from 'carbon-icons-svelte'
// todo: esc exits log view (change url)
// todo: memory leak? time out after 30 minutes or so? what about large data volume?
// track auto scroll state
let autoScroll = true
// copy logs helpers
let getLastNLines: (n: number) => string
let showNotification = false
let notificationTimeout: ReturnType<typeof setTimeout>
const handleCopy = async () => {
console.log('Copying logs')
const lastLines = getLastNLines(100)
try {
await navigator.clipboard.writeText(lastLines)
showNotification = true
// Clear existing timeout if there is one
if (notificationTimeout) {
clearTimeout(notificationTimeout)
}
// Hide notification after 2 seconds
notificationTimeout = setTimeout(() => {
showNotification = false
}, 2000)
} catch (err) {
console.error('Failed to copy logs:', err)
}
}
// clear copy notification timeout on destroy
onDestroy(() => {
if (notificationTimeout) {
clearTimeout(notificationTimeout)
}
})
// used in AnsiDisplay
let addMessage: (message: string) => void
Expand Down Expand Up @@ -164,9 +198,20 @@

<!-- Right section: Action buttons -->
<div class="flex items-center space-x-2">
<button class="p-1.5 text-gray-400 hover:text-gray-200 rounded-lg hover:bg-gray-800">
<Copy class="w-4 h-4" />
</button>
<div class="relative">
<button class="p-1.5 text-gray-400 hover:text-gray-200 rounded-lg hover:bg-gray-800" on:click={handleCopy}>
<Copy class="w-4 h-4" />
</button>
{#if showNotification}
<div
transition:fade={{ duration: 200 }}
class="absolute left-1/2 -translate-x-1/2 top-full mt-2 z-50 flex items-center gap-2 px-3 py-2 text-sm text-white bg-gray-900 rounded-lg shadow-lg border border-gray-800 whitespace-nowrap"
>
<CheckmarkOutline class="w-4 h-4" />
<span>Copied!</span>
</div>
{/if}
</div>
<button class="p-1.5 text-gray-400 hover:text-gray-200 rounded-lg hover:bg-gray-800">
<Download class="w-4 h-4" />
</button>
Expand Down Expand Up @@ -202,7 +247,7 @@
<!-- Logs content area -->
<div class="flex-1 min-h-0 m-1">
<div class="h-full bg-gray-900 rounded-md overflow-y-auto p-1">
<AnsiDisplay bind:addMessage bind:autoScroll />
<AnsiDisplay bind:addMessage bind:autoScroll bind:getLastNLines />
</div>
</div>
</div>
Expand Down

0 comments on commit 33f819d

Please sign in to comment.