Skip to content

Commit

Permalink
fix(web): make download logs actions work again (#1694)
Browse files Browse the repository at this point in the history
## Problem 

After changes introduced in
#1693, the `LogsButton`
component has become obsolete and does not work as expected.

## Solution

Use an old plain HTML link pointing to the expected URL and using the
[download](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download)
attribute.

## Additional note

Please, bear in mind that this is kind of intermediate change to make
the link work again. It will be superseded (and probably improved) by
the WIP #1690

## Testing

- Tested manually
  • Loading branch information
imobachgs authored Oct 23, 2024
2 parents b2eafff + eec8ebc commit 7dd6a1b
Show file tree
Hide file tree
Showing 3 changed files with 12 additions and 197 deletions.
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 <[email protected]>

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

-------------------------------------------------------------------
Wed Oct 23 15:26:29 UTC 2024 - Imobach Gonzalez Sosa <[email protected]>

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

0 comments on commit 7dd6a1b

Please sign in to comment.