Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(web): make download logs actions work again #1694

Merged
merged 2 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions web/package/agama-web-ui.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
-------------------------------------------------------------------
Wed Oct 23 16:26:29 UTC 2024 - David Diaz <dgonzalez@suse.com>

- Fix the link to download the logs (gh#agama-project/agama#1694).

-------------------------------------------------------------------
Wed Oct 23 15:26:29 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

Expand Down
100 changes: 2 additions & 98 deletions web/src/components/core/LogsButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import { screen } from "@testing-library/react";
import { installerRender } from "~/test-utils";
import { LogsButton } from "~/components/core";

const originalCreateElement = document.createElement;

const executor = jest.fn();
const fetchLogsFn = jest.fn();

Expand All @@ -50,102 +48,8 @@ afterAll(() => {
});

describe("LogsButton", () => {
it("renders a button for downloading logs", () => {
it("renders a link for downloading logs", () => {
installerRender(<LogsButton />);
screen.getByRole("button", { name: "Download logs" });
});

describe("when user clicks on it", () => {
it("inits download logs process", async () => {
const { user } = installerRender(<LogsButton />);
const button = screen.getByRole("button", { name: "Download logs" });
await user.click(button);
expect(fetchLogsFn).toHaveBeenCalled();
});

it("changes button text, puts it as disabled, and displays an informative alert", async () => {
const { user } = installerRender(<LogsButton />);

const button = screen.getByRole("button", { name: "Download logs" });
expect(button).not.toHaveAttribute("disabled");

await user.click(button);

expect(button.innerHTML).not.toContain("Download logs");
expect(button.innerHTML).toContain("Collecting logs...");
expect(button).toHaveAttribute("disabled");

const info = screen.queryByRole("heading", { name: /.*logs download as soon as.*/i });
const warning = screen.queryByRole("heading", { name: /.*went wrong*/i });

expect(info).toBeInTheDocument();
expect(warning).not.toBeInTheDocument();
});

describe("and logs are collected successfully", () => {
beforeEach(() => {
fetchLogsFn.mockResolvedValue({
blob: jest.fn().mockResolvedValue(new Blob(["testing"])),
});
});

it("triggers the download", async () => {
const { user } = installerRender(<LogsButton />);

// Ugly mocking needed here.
// Improvements are wanted and welcome.
// NOTE: document.createElement cannot mocked in beforeAll because it breaks all testsuite
// since its used internally by jsdom. Simply spying it is not enough because we want to
// mock only the call to the HTMLAnchorElement creation that happens when user clicks on the
// "Download logs".
// @ts-expect-error
document.originalCreateElement = originalCreateElement;

const anchorMock = document.createElement("a");
anchorMock.setAttribute = jest.fn();
anchorMock.click = jest.fn();

jest.spyOn(document, "createElement").mockImplementation((tag) => {
// @ts-expect-error
return tag === "a" ? anchorMock : document.originalCreateElement(tag);
});

// Now, let's simulate the "Download logs" user click
const button = screen.getByRole("button", { name: "Download logs" });
await user.click(button);

// And test what we're looking for
expect(document.createElement).toHaveBeenCalledWith("a");
expect(anchorMock).toHaveAttribute("href", "fake-blob-url");
expect(anchorMock).toHaveAttribute(
"download",
expect.stringMatching(/agama-installation-logs/),
);
expect(anchorMock.click).toHaveBeenCalled();

// Be polite and restore document.createElement function,
// although it should be done by the call to jest.restoreAllMocks()
// in the afterAll block
document.createElement = originalCreateElement;
});
});

describe("but the process fails", () => {
beforeEach(() => {
fetchLogsFn.mockRejectedValue("Sorry, something went wrong");
});

it("displays a warning alert along with the Download logs button", async () => {
const { user } = installerRender(<LogsButton />);

const button = screen.getByRole("button", { name: "Download logs" });
expect(button).not.toHaveAttribute("disabled");

await user.click(button);

expect(button.innerHTML).toContain("Download logs");
screen.getByRole("heading", { name: /.*went wrong.*try again.*/i });
});
});
screen.getByRole("link", { name: "Download logs" });
});
});
104 changes: 5 additions & 99 deletions web/src/components/core/LogsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,111 +20,17 @@
* find current contact information at www.suse.com.
*/

import React, { useState } from "react";
import { Alert, Button, ButtonProps } from "@patternfly/react-core";
import { Popup } from "~/components/core";
import React from "react";
import { _ } from "~/i18n";
import { useCancellablePromise } from "~/utils";
import { fetchLogs } from "~/api/manager";

const FILENAME = "agama-installation-logs.tar.gz";

/**
* Button for collecting and downloading Agama/YaST logs
*/
const LogsButton = (props: ButtonProps) => {
const { cancellablePromise } = useCancellablePromise();
const [error, setError] = useState(null);
const [isCollecting, setIsCollecting] = useState(false);

/**
* Helper function for triggering the download automatically
*
* @note Based on the article "Programmatic file downloads in the browser" found at
* https://blog.logrocket.com/programmatic-file-downloads-in-the-browser-9a5186298d5c
*
* @param {string} url - the file location to download from
*/
const autoDownload = (url: string) => {
const a = document.createElement("a");
a.href = url;
a.download = FILENAME;

// Click handler that releases the object URL after the element has been clicked
// This is required to let the browser know not to keep the reference to the file any longer
// See https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL
const clickHandler = () => {
setTimeout(() => {
URL.revokeObjectURL(url);
a.removeEventListener("click", clickHandler);
}, 150);
};

// Add the click event listener on the anchor element
a.addEventListener("click", clickHandler, false);

// Programmatically trigger a click on the anchor element
// Needed for make the download to happen automatically without attaching the anchor element to
// the DOM
a.click();
};

const collectAndDownload = () => {
setError(null);
setIsCollecting(true);
cancellablePromise(fetchLogs().then((response) => response.blob()))
.then(URL.createObjectURL)
.then(autoDownload)
.catch((error) => {
console.error(error);
setError(true);
})
.finally(() => setIsCollecting(false));
};

const close = () => setError(false);

const LogsButton = () => {
return (
<>
<Button
isInline
variant="link"
style={{ color: "white" }}
onClick={collectAndDownload}
isLoading={isCollecting}
isDisabled={isCollecting}
{...props}
>
{isCollecting ? _("Collecting logs...") : _("Download logs")}
</Button>

<Popup title={_("Download logs")} isOpen={isCollecting || error}>
{isCollecting && (
<Alert
isInline
isPlain
variant="info"
title={_(
"The browser will run the logs download as soon as they are ready. Please, be patient.",
)}
/>
)}

{error && (
<Alert
isInline
isPlain
variant="warning"
title={_("Something went wrong while collecting logs. Please, try again.")}
/>
)}
<Popup.Actions>
<Popup.Confirm onClick={close} autoFocus>
{_("Close")}
</Popup.Confirm>
</Popup.Actions>
</Popup>
</>
<a href="api/manager/logs.tar.gz" download>
{_("Download logs")}
</a>
);
};

Expand Down
Loading