|
1 | 1 | <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 }) |
58 | 26 | }
|
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' }) |
66 | 60 | }
|
| 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 | + } |
67 | 78 | </script>
|
68 | 79 |
|
69 | 80 | <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} |
111 | 120 | </div>
|
112 | 121 |
|
113 | 122 | <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 | + } |
150 | 159 | </style>
|
0 commit comments