Skip to content

Commit c18c0b2

Browse files
authored
feat(x2a): allow downloading logs from Logviewer (#2770)
* feat(x2a): allow downloading logs from Logviewer Copy is a bit hard, and It's good to have a way to donwload logs on the phases. FIX: FLPATH-3559 Signed-off-by: Eloy Coto <eloy.coto@acalustra.com> * fix(x2a): defer blob URL revocation to prevent download race condition URL.revokeObjectURL() was called synchronously after anchor.click(), which could race with the browser download start and cause failed or empty downloads. Defer revocation with setTimeout to allow the browser to initiate the download first. Signed-off-by: Eloy Coto <eloy.coto@acalustra.com> --------- Signed-off-by: Eloy Coto <eloy.coto@acalustra.com>
1 parent e62d73d commit c18c0b2

4 files changed

Lines changed: 108 additions & 0 deletions

File tree

workspaces/x2a/plugins/x2a/src/components/PhaseDetails.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { useClientService } from '../ClientService';
3636
import { ItemField } from './ItemField';
3737
import {
3838
canCancelPhase,
39+
downloadLogFile,
3940
formatDuration,
4041
humanizeDate,
4142
secondsBetween,
@@ -283,6 +284,9 @@ export const PhaseDetails = (
283284
<div style={{ height: 400 }}>
284285
<LogViewer
285286
text={logText || t('modulePage.phases.noLogsAvailable')}
287+
onDownloadLog={() =>
288+
downloadLogFile(logText || '', `${phaseName}-${projectId}`)
289+
}
286290
/>
287291
</div>
288292
)}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { downloadLogFile } from './downloadLogFile';
18+
19+
describe('downloadLogFile', () => {
20+
let createObjectURLMock: jest.Mock;
21+
let revokeObjectURLMock: jest.Mock;
22+
let anchorClickMock: jest.Mock;
23+
24+
beforeEach(() => {
25+
jest.useFakeTimers();
26+
createObjectURLMock = jest.fn().mockReturnValue('blob:mock-url');
27+
revokeObjectURLMock = jest.fn();
28+
URL.createObjectURL = createObjectURLMock;
29+
URL.revokeObjectURL = revokeObjectURLMock;
30+
31+
anchorClickMock = jest.fn();
32+
jest.spyOn(document, 'createElement').mockReturnValue({
33+
set href(_: string) {},
34+
set download(_: string) {},
35+
click: anchorClickMock,
36+
} as unknown as HTMLAnchorElement);
37+
});
38+
39+
afterEach(() => {
40+
jest.useRealTimers();
41+
jest.restoreAllMocks();
42+
});
43+
44+
it('creates a blob with the provided text', () => {
45+
downloadLogFile('log content', 'test');
46+
47+
expect(createObjectURLMock).toHaveBeenCalledWith(expect.any(Blob));
48+
});
49+
50+
it('triggers a download with the correct filename', () => {
51+
const setDownloadSpy = jest.fn();
52+
jest.spyOn(document, 'createElement').mockReturnValue({
53+
set href(_: string) {},
54+
set download(val: string) {
55+
setDownloadSpy(val);
56+
},
57+
click: anchorClickMock,
58+
} as unknown as HTMLAnchorElement);
59+
60+
downloadLogFile('log content', 'analyze-proj-1');
61+
62+
expect(setDownloadSpy).toHaveBeenCalledWith('analyze-proj-1.log');
63+
});
64+
65+
it('clicks the anchor to trigger download', () => {
66+
downloadLogFile('log content', 'test');
67+
68+
expect(anchorClickMock).toHaveBeenCalled();
69+
});
70+
71+
it('revokes the object URL after download', () => {
72+
downloadLogFile('log content', 'test');
73+
74+
expect(revokeObjectURLMock).not.toHaveBeenCalled();
75+
jest.runAllTimers();
76+
expect(revokeObjectURLMock).toHaveBeenCalledWith('blob:mock-url');
77+
});
78+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export const downloadLogFile = (text: string, name: string): void => {
18+
const blob = new Blob([text], { type: 'text/plain' });
19+
const url = URL.createObjectURL(blob);
20+
const anchor = document.createElement('a');
21+
anchor.href = url;
22+
anchor.download = `${name}.log`;
23+
anchor.click();
24+
setTimeout(() => URL.revokeObjectURL(url), 100);
25+
};

workspaces/x2a/plugins/x2a/src/components/tools/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * from './getLastPhaseReached';
2323
export * from './getNextPhase';
2424
export * from './canRunNextPhase';
2525
export * from './canCancelPhase';
26+
export * from './downloadLogFile';
2627
export * from './hasPhasePrerequisites';
2728
export * from './areEligibleModulesToRun';
2829
export * from './isEligibleForRetriggerInit';

0 commit comments

Comments
 (0)