Skip to content

Commit af7880c

Browse files
committed
Added logs buttons to images status modal
1 parent 793a08a commit af7880c

File tree

4 files changed

+137
-45
lines changed

4 files changed

+137
-45
lines changed

src/lib/common/job_utilities.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ const completeTracebackLine = 'Traceback (most recent call last):';
66
*
77
* @param {string|null} log
88
* @param {boolean} ignoreUppercaseTraceback
9+
* @param {boolean} imageError
910
* @returns {Array<{text: string, highlight: boolean}>}
1011
*/
11-
export function extractJobErrorParts(log = null, ignoreUppercaseTraceback = false) {
12+
export function extractJobErrorParts(log = null, ignoreUppercaseTraceback = false, imageError = false) {
1213
if (!log) {
1314
return [];
1415
}
@@ -19,12 +20,13 @@ export function extractJobErrorParts(log = null, ignoreUppercaseTraceback = fals
1920
if (
2021
log.startsWith('TASK ERROR') ||
2122
log.startsWith('JOB ERROR') ||
22-
log.startsWith('UNKNOWN ERROR')
23+
log.startsWith('UNKNOWN ERROR') ||
24+
imageError
2325
) {
2426
const lines = log.split('\n');
2527
if (lines.length > 1) {
2628
const [firstLine, ...nextLines] = lines;
27-
return [{ text: firstLine, highlight: true }, ...extractTraceback(nextLines.join('\n'))];
29+
return [{ text: firstLine, highlight: !imageError }, ...extractTraceback(nextLines.join('\n'))];
2830
}
2931
}
3032
return [{ text: log, highlight: false }];
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script>
2+
/** @type {Array<{text: string, highlight: boolean}>} */
3+
export let logParts = [];
4+
export let highlight = false;
5+
6+
/** Show/hide complete stack trace */
7+
let showDetails = false;
8+
9+
function expandDetails() {
10+
showDetails = true;
11+
// Restore focus on modal, otherwise it will not be possible to close it using the esc key
12+
const modal = document.querySelector('.modal.show');
13+
if (modal instanceof HTMLElement) {
14+
modal.focus();
15+
}
16+
}
17+
</script>
18+
19+
<div class="expandable-log">
20+
<!-- IMPORTANT: do not reindent the pre block, as it will affect the aspect of the log message -->
21+
{#if logParts.length > 1}
22+
<pre class="ps-0 pe-0">
23+
<!-- -->{#each logParts as part, i}{#if part.highlight}<div class="ps-3 pe-3 highlight">{part.text}
24+
<!-- --></div>{:else if showDetails || (i + 1 < logParts.length && !logParts[i + 1].highlight)}<div
25+
class="ps-3 pe-3">{part.text}</div>{:else}<button
26+
class="btn btn-link text-decoration-none details-btn"
27+
on:click={expandDetails}>... (details hidden, click here to expand)</button
28+
>{/if}{/each}</pre>
29+
{:else}
30+
<pre class:highlight>{logParts.map((p) => p.text).join('\n')}</pre>
31+
{/if}
32+
</div>
33+
34+
<style>
35+
.expandable-log {
36+
/** avoid issues with overflow of inner divs */
37+
display: table;
38+
width: 100%;
39+
}
40+
41+
.highlight {
42+
font-weight: bold;
43+
background-color: #ffe5e5;
44+
}
45+
46+
.details-btn {
47+
font-family: revert;
48+
}
49+
</style>

src/lib/components/jobs/ImagesStatusModal.svelte

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<script>
22
import { getAlertErrorFromResponse } from '$lib/common/errors';
3+
import { extractJobErrorParts } from '$lib/common/job_utilities';
4+
import ExpandableLog from '../common/ExpandableLog.svelte';
35
import Modal from '../common/Modal.svelte';
46
import Paginator from '../common/Paginator.svelte';
57
@@ -25,6 +27,11 @@
2527
/** @type {(import('fractal-components/types/api').Pagination & {images: string[]})|undefined} */
2628
let data = undefined;
2729
30+
let loadingLogs = false;
31+
let selectedLogImage = '';
32+
/** @type {Array<{text: string, highlight: boolean}>} */
33+
let logParts = [];
34+
2835
/**
2936
* @param {number} _projectId
3037
* @param {number} _datasetId
@@ -37,6 +44,9 @@
3744
data = undefined;
3845
page = 1;
3946
pageSize = 10;
47+
loadingLogs = false;
48+
logParts = [];
49+
selectedLogImage = '';
4050
projectId = _projectId;
4151
datasetId = _datasetId;
4252
workflowTaskId = _workflowTaskId;
@@ -70,22 +80,89 @@
7080
}
7181
loading = false;
7282
}
83+
84+
/**
85+
* @param {string} zarrUrl
86+
*/
87+
async function loadLogs(zarrUrl) {
88+
loadingLogs = true;
89+
const headers = new Headers();
90+
headers.set('Content-Type', 'application/json');
91+
const response = await fetch(
92+
`/api/v2/project/${projectId}/status/image-logs?workflowtask_id=${workflowTaskId}&dataset_id=${datasetId}&zarr_url=${zarrUrl}`,
93+
{
94+
method: 'POST',
95+
headers,
96+
body: JSON.stringify({
97+
workflowtask_id: workflowTaskId,
98+
dataset_id: datasetId,
99+
zarr_url: zarrUrl
100+
})
101+
}
102+
);
103+
if (!response.ok) {
104+
modal.displayErrorAlert(await getAlertErrorFromResponse(response));
105+
loadingLogs = false;
106+
return;
107+
}
108+
const { log } = await response.json();
109+
if (status === 'failed') {
110+
logParts = extractJobErrorParts(log, false, true);
111+
} else {
112+
logParts = [{ text: log, highlight: false }];
113+
}
114+
selectedLogImage = zarrUrl;
115+
loadingLogs = false;
116+
}
73117
</script>
74118

75-
<Modal id="imagesStatusModal" bind:this={modal} fullscreen={true}>
119+
<Modal id="imagesStatusModal" bind:this={modal} fullscreen={true} bodyCss="p-0">
76120
<svelte:fragment slot="header">
77-
<h1 class="modal-title fs-5">Images with status='{status}'</h1>
121+
<h1 class="modal-title fs-5">
122+
{#if selectedLogImage && !loadingLogs}
123+
Logs for {selectedLogImage}
124+
{:else}
125+
Images with status='{status}'
126+
{/if}
127+
</h1>
78128
</svelte:fragment>
79129
<svelte:fragment slot="body">
80130
<div id="errorAlert-imagesStatusModal" class="mb-2" />
81131
{#if loading && !data}
82132
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
133+
{:else if selectedLogImage && !loadingLogs}
134+
<div class="row">
135+
<div class="col">
136+
<ExpandableLog bind:logParts highlight={status === 'failed'} />
137+
</div>
138+
</div>
139+
<div class="row">
140+
<div class="col">
141+
<button class="m-2 ms-3 btn btn-primary" on:click={() => (selectedLogImage = '')}>
142+
Back
143+
</button>
144+
</div>
145+
</div>
83146
{:else if data}
84147
{#if data.images.length > 0}
85148
<table class="table table-striped">
86149
<tbody>
87150
{#each data.images as image}
88-
<tr><td>{image}</td></tr>
151+
<tr>
152+
<td>{image}</td>
153+
<td>
154+
<button class="btn btn-light" on:click={() => loadLogs(image)}>
155+
{#if loadingLogs}
156+
<span
157+
class="spinner-border spinner-border-sm"
158+
role="status"
159+
aria-hidden="true"
160+
/>
161+
{/if}
162+
<i class="bi-list-columns-reverse" /> Logs
163+
</button>
164+
</td>
165+
</tr>
89166
{/each}
90167
</tbody>
91168
</table>

src/lib/components/v2/jobs/JobLogsModal.svelte

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { extractJobErrorParts } from '$lib/common/job_utilities';
44
import { onDestroy } from 'svelte';
55
import Modal from '../../common/Modal.svelte';
6+
import ExpandableLog from '$lib/components/common/ExpandableLog.svelte';
67
78
/** @type {Array<{text: string, highlight: boolean}>} */
89
let logParts = [];
@@ -97,15 +98,6 @@
9798
}
9899
}
99100
100-
function expandDetails() {
101-
showDetails = true;
102-
// Restore focus on modal, otherwise it will not be possible to close it using the esc key
103-
const modal = document.querySelector('.modal.show');
104-
if (modal instanceof HTMLElement) {
105-
modal.focus();
106-
}
107-
}
108-
109101
function onClose() {
110102
showDetails = false;
111103
clearTimeout(updateJobTimeout);
@@ -120,7 +112,7 @@
120112
id="workflowJobLogsModal"
121113
fullscreen={true}
122114
bind:this={modal}
123-
bodyCss="bg-tertiary text-secondary"
115+
bodyCss="bg-tertiary text-secondary p-0 pt-2"
124116
{onClose}
125117
>
126118
<svelte:fragment slot="header">
@@ -136,36 +128,8 @@
136128
</div>
137129
Loading...
138130
{:else}
139-
<div class="row" id="workflow-job-logs">
140-
<!-- IMPORTANT: do not reindent the pre block, as it will affect the aspect of the log message -->
141-
{#if logParts.length > 1}
142-
<pre class="ps-0 pe-0">
143-
<!-- -->{#each logParts as part}{#if part.highlight}<div class="ps-3 pe-3 highlight">{part.text}
144-
<!-- --></div>{:else if showDetails}<div class="ps-3 pe-3">{part.text}</div>{:else}<button
145-
class="btn btn-link text-decoration-none details-btn"
146-
on:click={expandDetails}>... (details hidden, click here to expand)</button
147-
>{/if}{/each}</pre>
148-
{:else}
149-
<pre class:highlight={job.status === 'failed'}>{logParts
150-
.map((p) => p.text)
151-
.join('\n')}</pre>
152-
{/if}
153-
</div>
131+
<ExpandableLog bind:logParts highlight={job.status === 'failed'} />
154132
{/if}
155133
</svelte:fragment>
156134
</Modal>
157135

158-
<style>
159-
#workflow-job-logs pre {
160-
/** avoid issues with overflow of inner divs */
161-
display: table;
162-
}
163-
.highlight {
164-
font-weight: bold;
165-
background-color: #ffe5e5;
166-
}
167-
168-
.details-btn {
169-
font-family: revert;
170-
}
171-
</style>

0 commit comments

Comments
 (0)