Skip to content

Commit 37903ae

Browse files
committed
feat: download logs
1 parent 7633418 commit 37903ae

File tree

1 file changed

+149
-140
lines changed

1 file changed

+149
-140
lines changed
Lines changed: 149 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,159 @@
11
<script lang="ts">
2-
import api from "../api"
3-
import {beforeUpdate, onDestroy, onMount, tick} from "svelte"
4-
import stripAnsi from "strip-ansi"
5-
import Fa from "svelte-fa"
6-
import {faChevronCircleDown, faChevronCircleUp, faExpandArrowsAlt} from "@fortawesome/free-solid-svg-icons"
7-
import {messageBus} from "../messages/message-store"
8-
9-
export let container_id: string
10-
let oldContainerId: string
11-
let logElem: HTMLPreElement
12-
13-
let intervalHandler
14-
let loading: boolean = true
15-
let logs: string[] = []
16-
17-
let timestampMode: 'off' | 'local' | 'raw' = 'local'
18-
19-
beforeUpdate(() => {
20-
if (container_id !== oldContainerId) {
21-
oldContainerId = container_id
22-
loading = true
23-
logs = []
24-
if (container_id)
25-
api.send('get_container_logs', {container_id})
26-
}
27-
})
28-
29-
onMount(() => {
30-
api.register<string[]>('get_container_logs', loglines => {
31-
loading = false
32-
if (loglines?.length > 0) {
33-
let scrollToBottom = !logs || (logElem && logElem.scrollTop >= logElem.scrollHeight - logElem.clientHeight)
34-
const totalLines = logs.length + loglines.length
35-
if (totalLines > 5000) {
36-
logs.slice(totalLines - 5000)
37-
}
38-
logs.push(...loglines.map(line => stripAnsi(line)))
39-
if (scrollToBottom)
40-
tick().then(() => logElem.scrollTop = logElem.scrollHeight)
41-
}
42-
}, (err) => messageBus.add({text: err, type: 'error'}))
43-
44-
intervalHandler = setInterval(() => {
45-
api.send('get_container_logs', {container_id, onlynew: true})
46-
}, 3000)
47-
})
48-
49-
onDestroy(() => {
50-
clearInterval(intervalHandler)
51-
api.unregister('get_container_logs')
52-
})
53-
54-
function fullscreen() {
55-
if (logElem?.requestFullscreen) {
56-
logElem.requestFullscreen({navigationUI: 'show'})
57-
}
2+
import api from '../api'
3+
import { beforeUpdate, onDestroy, onMount, tick } from 'svelte'
4+
import stripAnsi from 'strip-ansi'
5+
import Fa from 'svelte-fa'
6+
import { faChevronCircleDown, faChevronCircleUp, faDownload, faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons'
7+
import { messageBus } from '../messages/message-store'
8+
import { downloadBlob } from './utils'
9+
10+
export let container_id: string
11+
let oldContainerId: string
12+
let logElem: HTMLPreElement
13+
14+
let intervalHandler
15+
let loading: boolean = true
16+
let logs: string[] = []
17+
18+
let timestampMode: 'off' | 'local' | 'raw' = 'local'
19+
20+
beforeUpdate(() => {
21+
if (container_id !== oldContainerId) {
22+
oldContainerId = container_id
23+
loading = true
24+
logs = []
25+
if (container_id) api.send('get_container_logs', { container_id })
5826
}
59-
60-
function fmtTimestamp(value: string) {
61-
return new Date(value).toLocaleDateString(undefined, {
62-
weekday: 'short',
63-
year: 'numeric', month: '2-digit', day: '2-digit',
64-
hour: '2-digit', minute: '2-digit', second: '2-digit',
65-
})
27+
})
28+
29+
onMount(() => {
30+
api.register<string[]>(
31+
'get_container_logs',
32+
(loglines) => {
33+
loading = false
34+
if (loglines?.length > 0) {
35+
let scrollToBottom = !logs || (logElem && logElem.scrollTop >= logElem.scrollHeight - logElem.clientHeight)
36+
const totalLines = logs.length + loglines.length
37+
if (totalLines > 5000) {
38+
logs.slice(totalLines - 5000)
39+
}
40+
logs.push(...loglines.map((line) => stripAnsi(line)))
41+
if (scrollToBottom) tick().then(() => (logElem.scrollTop = logElem.scrollHeight))
42+
}
43+
},
44+
(err) => messageBus.add({ text: err, type: 'error' })
45+
)
46+
47+
intervalHandler = setInterval(() => {
48+
api.send('get_container_logs', { container_id, onlynew: true })
49+
}, 3000)
50+
})
51+
52+
onDestroy(() => {
53+
clearInterval(intervalHandler)
54+
api.unregister('get_container_logs')
55+
})
56+
57+
function fullscreen() {
58+
if (logElem?.requestFullscreen) {
59+
logElem.requestFullscreen({ navigationUI: 'show' })
6660
}
61+
}
62+
63+
function downloadLogs() {
64+
downloadBlob(new Blob(logs, { type: 'text/plain' }), container_id + '.log')
65+
}
66+
67+
function fmtTimestamp(value: string) {
68+
return new Date(value).toLocaleDateString(undefined, {
69+
weekday: 'short',
70+
year: 'numeric',
71+
month: '2-digit',
72+
day: '2-digit',
73+
hour: '2-digit',
74+
minute: '2-digit',
75+
second: '2-digit',
76+
})
77+
}
6778
</script>
6879

6980
<div>
70-
{#if loading}
71-
<div class="d-flex align-items-center">
72-
<strong>Loading...</strong>
73-
<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>
74-
</div>
75-
{:else}
76-
{#if logs.length === 0}
77-
<p>No Log Output yet ...</p>
78-
{:else}
79-
<div class="d-flex justify-content-between align-items-center mb-2">
80-
<div>
81-
<select class="form-select" bind:value={timestampMode}>
82-
<option value="off">hide timestamps</option>
83-
<option value="local">local timestamps</option>
84-
<option value="raw">raw timestamps</option>
85-
</select>
86-
</div>
87-
<div>
88-
<button type="button" class="btn mx-1" title="Scroll to Top" on:click={() => logElem.scrollTop = 0}>
89-
<Fa icon={faChevronCircleUp} size="lg" color="#666"/>
90-
</button>
91-
<button type="button" class="btn mx-1" title="Scroll to Bottom"
92-
on:click={() => logElem.scrollTop = logElem.scrollHeight}>
93-
<Fa icon={faChevronCircleDown} size="lg" color="#666"/>
94-
</button>
95-
<button type="button" class="btn mx-1" title="Fullscreen" on:click={fullscreen}>
96-
<Fa icon={faExpandArrowsAlt} size="lg" color="#666"/>
97-
</button>
98-
</div>
99-
</div>
100-
<!-- this might look ugly but it is necessary because whitespaces are preserved in <pre> envs-->
101-
<pre bind:this={logElem} class="mb-0">{#each logs as line}{
102-
@const timestamp = line.substring(0, line.indexOf(' ')) }{
103-
@const text = line.substring(line.indexOf(' ') + 1)
104-
}<span class="pe-3">{
105-
#if timestampMode !== 'off'}<time datetime={timestamp}
106-
class="me-1 px-1">{#if timestampMode === 'local'
107-
}{fmtTimestamp(timestamp)}{:else}{timestamp}{/if}</time>{/if
108-
}<span class="pe-3">{text}</span></span>{/each}</pre>
109-
{/if}
110-
{/if}
81+
{#if loading}
82+
<div class="d-flex align-items-center">
83+
<strong>Loading...</strong>
84+
<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>
85+
</div>
86+
{:else if logs.length === 0}
87+
<p>No Log Output yet ...</p>
88+
{:else}
89+
<div class="d-flex justify-content-between align-items-center mb-2">
90+
<div>
91+
<select class="form-select" bind:value="{timestampMode}">
92+
<option value="off">hide timestamps</option>
93+
<option value="local">local timestamps</option>
94+
<option value="raw">raw timestamps</option>
95+
</select>
96+
</div>
97+
<div>
98+
<button type="button" class="btn mx-1" title="Scroll to Top" on:click="{() => (logElem.scrollTop = 0)}">
99+
<Fa icon="{faChevronCircleUp}" size="lg" color="#666" />
100+
</button>
101+
<button type="button" class="btn mx-1" title="Scroll to Bottom" on:click="{() => (logElem.scrollTop = logElem.scrollHeight)}">
102+
<Fa icon="{faChevronCircleDown}" size="lg" color="#666" />
103+
</button>
104+
<button type="button" class="btn mx-1" title="Download shown log lines" on:click="{downloadLogs}">
105+
<Fa icon="{faDownload}" size="lg" color="#666" />
106+
</button>
107+
<button type="button" class="btn mx-1" title="Fullscreen" on:click="{fullscreen}">
108+
<Fa icon="{faExpandArrowsAlt}" size="lg" color="#666" />
109+
</button>
110+
</div>
111+
</div>
112+
<!-- this might look ugly but it is necessary because whitespaces are preserved in <pre> envs-->
113+
<pre bind:this="{logElem}" class="mb-0">{#each logs as line}{@const timestamp = line.substring(0, line.indexOf(' '))}{@const text = line.substring(line.indexOf(' ') + 1)}<span
114+
class="pe-3"
115+
>{#if timestampMode !== 'off'}<time datetime="{timestamp}" class="me-1 px-1"
116+
>{#if timestampMode === 'local'}{fmtTimestamp(timestamp)}{:else}{timestamp}{/if}</time
117+
>{/if}<span class="pe-3">{text}</span></span
118+
>{/each}</pre>
119+
{/if}
111120
</div>
112121

113122
<style>
114-
pre {
115-
scroll-behavior: smooth;
116-
max-height: calc(100vh - 280px);
117-
outline: 1px solid #ccc9;
118-
padding: .1rem .1rem .25rem .1rem;
119-
border-radius: 3px;
120-
}
121-
122-
pre > span {
123-
display: block;
124-
}
125-
126-
pre > span:nth-child(even) {
127-
background-color: #99999923;
128-
width: fit-content;
129-
border-top-right-radius: 5px;
130-
border-bottom-right-radius: 5px;
131-
}
132-
133-
pre time {
134-
display: inline-block;
135-
background-color: #99666615;
136-
}
137-
138-
:global(html.dark) pre time {
139-
background-color: #cc999913;
140-
}
141-
142-
:global(html.dark) pre {
143-
background-color: var(--bs-body-bg-alt);
144-
}
145-
146-
:global(html:not(.dark)) pre {
147-
background-color: #eee;
148-
}
149-
123+
pre {
124+
scroll-behavior: smooth;
125+
max-height: calc(100vh - 280px);
126+
outline: 1px solid #ccc9;
127+
padding: 0.1rem 0.1rem 0.25rem 0.1rem;
128+
border-radius: 3px;
129+
display: flex;
130+
flex-direction: column;
131+
}
132+
133+
pre > span {
134+
display: block;
135+
}
136+
137+
pre > span:nth-child(even) {
138+
background-color: #99999923;
139+
border-top-right-radius: 5px;
140+
border-bottom-right-radius: 5px;
141+
}
142+
143+
pre time {
144+
display: inline-block;
145+
background-color: #99666615;
146+
}
147+
148+
:global(html.dark) pre time {
149+
background-color: #cc999913;
150+
}
151+
152+
:global(html.dark) pre {
153+
background-color: var(--bs-body-bg-alt);
154+
}
155+
156+
:global(html:not(.dark)) pre {
157+
background-color: #eee;
158+
}
150159
</style>

0 commit comments

Comments
 (0)