mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
467 lines
12 KiB
Svelte
467 lines
12 KiB
Svelte
<script lang="ts" context="module">
|
|
const s3LogPrefixes = [
|
|
'\n[windmill] Previous logs have been saved to object storage at logs/',
|
|
'\n[windmill] Previous logs have been saved to disk at logs/',
|
|
'\n[windmill] No object storage set in instance settings. Previous logs have been saved to disk at logs/'
|
|
]
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { ClipboardCopy, Download, Expand, Loader2 } from 'lucide-svelte'
|
|
import { Button, Drawer, DrawerContent } from './common'
|
|
import { copyToClipboard } from '$lib/utils'
|
|
import { base } from '$lib/base'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import AnsiUp from 'ansi_up'
|
|
import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte'
|
|
import { JobService } from '$lib/gen'
|
|
import Tooltip from './Tooltip.svelte'
|
|
|
|
export let content: string | undefined
|
|
export let isLoading: boolean
|
|
export let duration: number | undefined = undefined
|
|
export let mem: number | undefined = undefined
|
|
export let wrapperClass = ''
|
|
export let jobId: string | undefined = undefined
|
|
export let tag: string | undefined
|
|
export let small = false
|
|
export let drawerOpen = false
|
|
export let noMaxH = false
|
|
export let noAutoScroll = false
|
|
export let download = true
|
|
export let customEmptyMessage = 'No logs are available yet'
|
|
|
|
// @ts-ignore
|
|
const ansi_up = new AnsiUp()
|
|
|
|
ansi_up.use_classes = true
|
|
|
|
let scroll = true
|
|
let div: HTMLElement | null = null
|
|
|
|
// let downloadStartUrl: string | undefined = undefined
|
|
|
|
let LOG_INC = 10000
|
|
let LOG_LIMIT = LOG_INC
|
|
|
|
let lastJobId = jobId
|
|
|
|
let loadedFromObjectStore = ''
|
|
$: if (jobId !== lastJobId) {
|
|
lastJobId = jobId
|
|
loadedFromObjectStore = ''
|
|
LOG_LIMIT = LOG_INC
|
|
}
|
|
|
|
function findPrefixIndex(truncateContent: string): number | undefined {
|
|
let index = s3LogPrefixes.findIndex((x) => truncateContent.startsWith(x))
|
|
if (index == -1) {
|
|
return undefined
|
|
}
|
|
return index
|
|
}
|
|
function findStartUrl(truncateContent: string, prefixIndex: number | undefined = undefined) {
|
|
if (prefixIndex == undefined) {
|
|
return undefined
|
|
}
|
|
const end = truncateContent.substring(1).indexOf('\n')
|
|
return prefixIndex != undefined && truncateContent
|
|
? truncateContent.substring(
|
|
s3LogPrefixes[prefixIndex]?.length,
|
|
end == -1 ? undefined : end + 1
|
|
)
|
|
: undefined
|
|
}
|
|
|
|
function tooltipText(prefixIndex: number | undefined) {
|
|
if (prefixIndex == undefined) {
|
|
return 'No path/file detected to download from'
|
|
} else if (prefixIndex == 0) {
|
|
return 'Download the previous logs from the instance configured object store'
|
|
} else if (prefixIndex == 1) {
|
|
return 'Attempt to download the logs from disk. Assume there is a shared disk between the workers and the server at /tmp/windmill/logs. Upgrade to EE to use an object store such as S3 instead of a shared volume.'
|
|
} else if (prefixIndex == 2) {
|
|
return 'Attempt to download the logs from disk. Assume there is a shared disk between the workers and the server at /tmp/windmill/logs. Since you are on EE, you can alternatively use an object store such as S3 configured in the instance settings instead of a shared volume..'
|
|
}
|
|
}
|
|
|
|
$: truncatedContent = truncateContent(content, loadedFromObjectStore, LOG_LIMIT)
|
|
|
|
$: prefixIndex = findPrefixIndex(truncatedContent)
|
|
$: downloadStartUrl = findStartUrl(truncatedContent, prefixIndex)
|
|
|
|
function truncateContent(
|
|
jobContent: string | undefined,
|
|
loadedFromObjectStore: string,
|
|
limit: number
|
|
) {
|
|
let content = loadedFromObjectStore + (jobContent ?? '')
|
|
if (content.length > limit) {
|
|
return content.substring(content.length - limit)
|
|
}
|
|
return content
|
|
}
|
|
|
|
$: truncatedContent && scrollToBottom()
|
|
|
|
$: html = ansi_up.ansi_to_html(
|
|
downloadStartUrl && prefixIndex != undefined
|
|
? truncatedContent.substring(
|
|
truncatedContent.substring(1).indexOf('\n') + 2,
|
|
truncatedContent.length
|
|
)
|
|
: truncatedContent
|
|
)
|
|
export function scrollToBottom() {
|
|
scroll && setTimeout(() => div?.scroll({ top: div?.scrollHeight, behavior: 'smooth' }), 100)
|
|
}
|
|
|
|
let logViewer: Drawer
|
|
|
|
async function getStoreLogs() {
|
|
if (downloadStartUrl) {
|
|
scroll = false
|
|
let res = (await JobService.getLogFileFromStore({
|
|
workspace: $workspaceStore ?? '',
|
|
path: downloadStartUrl
|
|
})) as string
|
|
downloadStartUrl = undefined
|
|
LOG_LIMIT += Math.min(LOG_INC, res.length)
|
|
loadedFromObjectStore = res + loadedFromObjectStore
|
|
let newC = truncateContent(content, loadedFromObjectStore, LOG_LIMIT)
|
|
LOG_LIMIT -= newC.indexOf('\n') + 1
|
|
} else {
|
|
console.error('No file detected to download from')
|
|
}
|
|
}
|
|
|
|
function showMoreTruncate(len: number) {
|
|
scroll = false
|
|
LOG_LIMIT += LOG_INC
|
|
console.log(LOG_INC, len, LOG_LIMIT)
|
|
let newC = truncateContent(content, loadedFromObjectStore, LOG_LIMIT)
|
|
let newlineIndex = newC.indexOf('\n') + 1
|
|
if (newlineIndex < LOG_INC / 2) {
|
|
LOG_LIMIT -= newlineIndex
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Drawer bind:this={logViewer} bind:open={drawerOpen} size="900px">
|
|
<DrawerContent title="Expanded Logs" on:close={logViewer.closeDrawer}>
|
|
{#snippet actions()}
|
|
{#if jobId && download}
|
|
<Button
|
|
href="{base}/api/w/{$workspaceStore}/jobs_u/get_logs/{jobId}"
|
|
download="windmill_logs_{jobId}.txt"
|
|
color="light"
|
|
size="xs"
|
|
startIcon={{
|
|
icon: Download
|
|
}}
|
|
>
|
|
Download
|
|
</Button>
|
|
{/if}
|
|
|
|
<Button
|
|
on:click={() => copyToClipboard(content)}
|
|
color="light"
|
|
size="xs"
|
|
startIcon={{
|
|
icon: ClipboardCopy
|
|
}}
|
|
>
|
|
Copy to clipboard
|
|
</Button>
|
|
{/snippet}
|
|
<div>
|
|
<pre
|
|
class="bg-surface-secondary text-secondary text-xs w-full p-2 whitespace-pre-wrap border rounded-md"
|
|
>{#if content}{@const len =
|
|
(content?.length ?? 0) +
|
|
(loadedFromObjectStore?.length ?? 0)}{#if downloadStartUrl}<button
|
|
on:click={getStoreLogs}
|
|
>Show more... <Tooltip>{tooltipText(prefixIndex)}</Tooltip></button
|
|
><br />{:else if len > LOG_LIMIT}(truncated to the last {LOG_LIMIT} characters)...<br
|
|
/><button on:click={() => showMoreTruncate(len)}>Show more..</button><br
|
|
/>{/if}{@html html}{:else if isLoading}Waiting for job to start...{:else}No logs are available yet{/if}</pre
|
|
>
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
<div class="relative w-full h-full {wrapperClass}">
|
|
<div
|
|
bind:this={div}
|
|
class="w-full h-full overflow-auto relative bg-surface-secondary {noMaxH ? '' : 'max-h-screen'}"
|
|
>
|
|
<div class="sticky z-10 top-0 right-0 w-full flex flex-row-reverse justify-between text-sm">
|
|
<div class="flex gap-2 pl-0.5 bg-surface-secondary">
|
|
{#if jobId && download}
|
|
<div class="flex items-center">
|
|
<a
|
|
class="text-primary pb-0.5"
|
|
target="_blank"
|
|
href="{base}/api/w/{$workspaceStore}/jobs_u/get_logs/{jobId}"
|
|
download="windmill_logs_{jobId}.txt"
|
|
><Download size="14" />
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
<button on:click={logViewer.openDrawer}><Expand size="12" /></button>
|
|
{#if !noAutoScroll}
|
|
<div
|
|
class="{small ? '' : 'py-2'} pr-2 {small
|
|
? '!text-2xs'
|
|
: '!text-xs'} flex gap-2 text-tertiary items-center"
|
|
>
|
|
Auto scroll
|
|
<input class="windmillapp" type="checkbox" bind:checked={scroll} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{#if isLoading}
|
|
<div class="flex gap-2 absolute top-2 left-2 items-center z-10">
|
|
<Loader2 class="animate-spin" />
|
|
{#if tag}
|
|
<div class="flex flex-row items-center gap-1">
|
|
<div class="text-secondary {small ? '!text-2xs' : '!text-xs'}">tag: {tag}</div>
|
|
<NoWorkerWithTagWarning {tag} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else if duration}
|
|
<span
|
|
class="absolute {small ? '!text-2xs' : '!text-xs'} text-tertiary dark:text-gray-400 {small
|
|
? 'top-0'
|
|
: 'top-2'} left-2">took {duration}ms</span
|
|
>
|
|
{/if}
|
|
{#if mem}
|
|
<span
|
|
class="absolute {small ? '!text-2xs' : '!text-xs'} text-tertiary dark:text-gray-400 {small
|
|
? 'top-0'
|
|
: 'top-2'} left-36">mem peak: {(mem / 1024).toPrecision(4)}MB</span
|
|
>
|
|
{/if}
|
|
<pre class="whitespace-pre break-words {small ? '!text-2xs' : '!text-xs'} w-full p-2"
|
|
>{#if content}{@const len =
|
|
(content?.length ?? 0) +
|
|
(loadedFromObjectStore?.length ?? 0)}{#if downloadStartUrl}<button on:click={getStoreLogs}
|
|
>Show more... <Tooltip>{tooltipText(prefixIndex)}</Tooltip></button
|
|
><br />{:else if len > LOG_LIMIT}(truncated to the last {LOG_LIMIT} characters)<br
|
|
/><button on:click={() => showMoreTruncate(len)}>Show more..</button><br />{/if}<span
|
|
>{@html html}</span
|
|
>{:else if !isLoading}<span>{customEmptyMessage}</span>{/if}</pre
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<style global>
|
|
/* Foreground colors */
|
|
.ansi-black-fg {
|
|
color: rgb(0, 0, 0);
|
|
}
|
|
.ansi-red-fg {
|
|
color: rgb(187, 0, 0);
|
|
}
|
|
.ansi-green-fg {
|
|
color: rgb(0, 187, 0);
|
|
}
|
|
.ansi-yellow-fg {
|
|
color: rgb(187, 187, 0);
|
|
}
|
|
.ansi-blue-fg {
|
|
color: rgb(0, 0, 187);
|
|
}
|
|
.ansi-magenta-fg {
|
|
color: rgb(187, 0, 187);
|
|
}
|
|
.ansi-cyan-fg {
|
|
color: rgb(0, 187, 187);
|
|
}
|
|
.ansi-white-fg {
|
|
color: rgb(255, 255, 255);
|
|
}
|
|
|
|
.ansi-bright-black-fg {
|
|
color: rgb(85, 85, 85);
|
|
}
|
|
.ansi-bright-red-fg {
|
|
color: rgb(255, 85, 85);
|
|
}
|
|
.ansi-bright-green-fg {
|
|
color: rgb(0, 255, 0);
|
|
}
|
|
.ansi-bright-yellow-fg {
|
|
color: rgb(255, 255, 85);
|
|
}
|
|
.ansi-bright-blue-fg {
|
|
color: rgb(85, 85, 255);
|
|
}
|
|
.ansi-bright-magenta-fg {
|
|
color: rgb(255, 85, 255);
|
|
}
|
|
.ansi-bright-cyan-fg {
|
|
color: rgb(85, 255, 255);
|
|
}
|
|
.ansi-bright-white-fg {
|
|
color: rgb(255, 255, 255);
|
|
}
|
|
|
|
/* Background colors */
|
|
.ansi-black-bg {
|
|
background-color: rgb(0, 0, 0);
|
|
}
|
|
.ansi-red-bg {
|
|
background-color: rgb(187, 0, 0);
|
|
}
|
|
.ansi-green-bg {
|
|
background-color: rgb(0, 187, 0);
|
|
}
|
|
.ansi-yellow-bg {
|
|
background-color: rgb(187, 187, 0);
|
|
}
|
|
.ansi-blue-bg {
|
|
background-color: rgb(0, 0, 187);
|
|
}
|
|
.ansi-magenta-bg {
|
|
background-color: rgb(187, 0, 187);
|
|
}
|
|
.ansi-cyan-bg {
|
|
background-color: rgb(0, 187, 187);
|
|
}
|
|
.ansi-white-bg {
|
|
background-color: rgb(255, 255, 255);
|
|
}
|
|
|
|
.ansi-bright-black-bg {
|
|
background-color: rgb(85, 85, 85);
|
|
}
|
|
.ansi-bright-red-bg {
|
|
background-color: rgb(255, 85, 85);
|
|
}
|
|
.ansi-bright-green-bg {
|
|
background-color: rgb(0, 255, 0);
|
|
}
|
|
.ansi-bright-yellow-bg {
|
|
background-color: rgb(255, 255, 85);
|
|
}
|
|
.ansi-bright-blue-bg {
|
|
background-color: rgb(85, 85, 255);
|
|
}
|
|
.ansi-bright-magenta-bg {
|
|
background-color: rgb(255, 85, 255);
|
|
}
|
|
.ansi-bright-cyan-bg {
|
|
background-color: rgb(85, 255, 255);
|
|
}
|
|
.ansi-bright-white-bg {
|
|
background-color: rgb(255, 255, 255);
|
|
}
|
|
|
|
/* Foreground colors for dark mode (Nord theme) */
|
|
.dark .ansi-black-fg {
|
|
color: rgb(46, 52, 64);
|
|
}
|
|
.dark .ansi-red-fg {
|
|
color: rgb(191, 97, 106);
|
|
}
|
|
.dark .ansi-green-fg {
|
|
color: rgb(163, 190, 140);
|
|
}
|
|
.dark .ansi-yellow-fg {
|
|
color: rgb(235, 203, 139);
|
|
}
|
|
.dark .ansi-blue-fg {
|
|
color: rgb(94, 129, 172);
|
|
}
|
|
.dark .ansi-magenta-fg {
|
|
color: rgb(180, 142, 173);
|
|
}
|
|
.dark .ansi-cyan-fg {
|
|
color: rgb(136, 192, 208);
|
|
}
|
|
.dark .ansi-white-fg {
|
|
color: rgb(216, 222, 233);
|
|
}
|
|
|
|
.dark .ansi-bright-black-fg {
|
|
color: rgb(67, 76, 94);
|
|
}
|
|
.dark .ansi-bright-red-fg {
|
|
color: rgb(191, 97, 106);
|
|
}
|
|
.dark .ansi-bright-green-fg {
|
|
color: rgb(163, 190, 140);
|
|
}
|
|
.dark .ansi-bright-yellow-fg {
|
|
color: rgb(235, 203, 139);
|
|
}
|
|
.dark .ansi-bright-blue-fg {
|
|
color: rgb(94, 129, 172);
|
|
}
|
|
.dark .ansi-bright-magenta-fg {
|
|
color: rgb(180, 142, 173);
|
|
}
|
|
.dark .ansi-bright-cyan-fg {
|
|
color: rgb(136, 192, 208);
|
|
}
|
|
.dark .ansi-bright-white-fg {
|
|
color: rgb(229, 233, 240);
|
|
}
|
|
|
|
/* Background colors for dark mode (Nord theme) */
|
|
.dark .ansi-black-bg {
|
|
background-color: rgb(46, 52, 64);
|
|
}
|
|
.dark .ansi-red-bg {
|
|
background-color: rgb(191, 97, 106);
|
|
}
|
|
.dark .ansi-green-bg {
|
|
background-color: rgb(163, 190, 140);
|
|
}
|
|
.dark .ansi-yellow-bg {
|
|
background-color: rgb(235, 203, 139);
|
|
}
|
|
.dark .ansi-blue-bg {
|
|
background-color: rgb(94, 129, 172);
|
|
}
|
|
.dark .ansi-magenta-bg {
|
|
background-color: rgb(180, 142, 173);
|
|
}
|
|
.dark .ansi-cyan-bg {
|
|
background-color: rgb(136, 192, 208);
|
|
}
|
|
.dark .ansi-white-bg {
|
|
background-color: rgb(216, 222, 233);
|
|
}
|
|
|
|
.dark .ansi-bright-black-bg {
|
|
background-color: rgb(67, 76, 94);
|
|
}
|
|
.dark .ansi-bright-red-bg {
|
|
background-color: rgb(191, 97, 106);
|
|
}
|
|
.dark .ansi-bright-green-bg {
|
|
background-color: rgb(163, 190, 140);
|
|
}
|
|
.dark .ansi-bright-yellow-bg {
|
|
background-color: rgb(235, 203, 139);
|
|
}
|
|
.dark .ansi-bright-blue-bg {
|
|
background-color: rgb(94, 129, 172);
|
|
}
|
|
.dark .ansi-bright-magenta-bg {
|
|
background-color: rgb(180, 142, 173);
|
|
}
|
|
.dark .ansi-bright-cyan-bg {
|
|
background-color: rgb(136, 192, 208);
|
|
}
|
|
.dark .ansi-bright-white-bg {
|
|
background-color: rgb(229, 233, 240);
|
|
}
|
|
</style>
|