Skip to content

Commit

Permalink
Merge pull request #407 from yast/show_logs
Browse files Browse the repository at this point in the history
Added a button for displaying the YaST logs
  • Loading branch information
lslezak committed Feb 6, 2023
2 parents a28b018 + c4caae7 commit f605a5d
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 1 deletion.
6 changes: 6 additions & 0 deletions web/package/cockpit-d-installer.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Tue Jan 24 09:32:13 UTC 2023 - Ladislav Slezák <lslezak@suse.com>

- Added a button for displaying the YaST logs
(related to gh#yast/d-installer#379)

-------------------------------------------------------------------
Fri Jan 20 09:03:22 UTC 2023 - David Diaz <dgonzalez@suse.com>

Expand Down
8 changes: 8 additions & 0 deletions web/src/assets/styles/blocks.scss
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,11 @@ section > .content {
.sidebar[data-state="visible"] {
transition: all 0.2s ease-in-out;
}

// raw file content with formatting similar to <pre>
.filecontent {
font-family: var(--ff-code);
font-size: 90%;
word-break: break-all;
white-space: pre-wrap;
}
7 changes: 7 additions & 0 deletions web/src/assets/styles/utilities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,10 @@
.bottom-shadow {
box-shadow: 0px -3px 10px 0px var(--color-gray-darker);
}

.tallest {
/** block-size fallbacks **/
height: 95dvh;
/** END block-size fallbacks **/
block-size: 95dvh;
}
84 changes: 84 additions & 0 deletions web/src/components/core/FileViewer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) [2023] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of version 2 of the GNU General Public License as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import React, { useState, useEffect } from "react";
import { Popup } from "~/components/core";
import { Alert } from "@patternfly/react-core";
import { LoadingEnvironment } from "~/components/layout";

import cockpit from "../../lib/cockpit";

export default function FileViewer({ file, title, onCloseCallback }) {
// the popup is visible
const [isOpen, setIsOpen] = useState(true);
// error message for failed load
const [error, setError] = useState(null);
// the file content
const [content, setContent] = useState("");
// current state
const [state, setState] = useState("loading");

useEffect(() => {
// NOTE: reading non-existing files in cockpit does not fail, the result is null
// see https://cockpit-project.org/guide/latest/cockpit-file
cockpit.file(file).read()
.then((data) => {
setState("ready");
setContent(data);
})
.catch((data) => {
setState("ready");
setError(data.message);
});
}, [file]);

const close = () => {
setIsOpen(false);
if (onCloseCallback) onCloseCallback();
};

return (
<Popup
isOpen={isOpen}
title={title || file}
variant="large"
className="tallest"
>
{ state === "loading" && <LoadingEnvironment text="Reading file..." /> }
{ (content === null || error) &&
<Alert
isInline
isPlain
variant="warning"
title="Cannot read the file"
>
{error}
</Alert> }
<div className="filecontent">
{content}
</div>

<Popup.Actions>
<Popup.Confirm onClick={close} autoFocus>Close</Popup.Confirm>
</Popup.Actions>
</Popup>
);
}
118 changes: 118 additions & 0 deletions web/src/components/core/FileViewer.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (c) [2023] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of version 2 of the GNU General Public License as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import React from "react";

import { screen, waitFor, within } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { FileViewer } from "~/components/core";
import cockpit from "../../lib/cockpit";

jest.mock("../../lib/cockpit");

const readFn = jest.fn((arg) => new Promise(jest.fn()));

const fileFn = jest.fn();
fileFn.mockImplementation(() => {
return {
read: readFn
}
});

cockpit.file.mockImplementation(fileFn);

// testing data
const file_name = "/testfile"
const content = "Read file content";
const title = "YaST Logs";

describe("FileViewer", () => {
beforeEach(() => {
readFn.mockResolvedValue(content);
});

it("displays the specified file and the title", async () => {
plainRender(<FileViewer file={file_name} title={title} />);
const dialog = await screen.findByRole("dialog");

// the file was read from cockpit
expect(fileFn).toHaveBeenCalledWith(file_name);
expect(readFn).toHaveBeenCalled();

within(dialog).getByText(title);
within(dialog).getByText(content);
});

it("displays the file name when the title is missing", async () => {
plainRender(<FileViewer file={file_name} />);
const dialog = await screen.findByRole("dialog");

within(dialog).getByText(file_name);
});

it("closes the popup after clicking the close button", async () => {
const { user } = plainRender(<FileViewer file={file_name} title={title} />);
const dialog = await screen.findByRole("dialog");
const closeButton = within(dialog).getByRole("button", { name: /Close/i });

await user.click(closeButton);
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});

it("triggers the onCloseCallback after clicking the close button", async () => {
const callback = jest.fn();
const { user } = plainRender(<FileViewer file={file_name} title={title} onCloseCallback={callback} />);
const dialog = await screen.findByRole("dialog");
const closeButton = within(dialog).getByRole("button", { name: /Close/i });

await user.click(closeButton);

expect(callback).toHaveBeenCalled();
});

describe("when the file does not exist", () => {
beforeEach(() => {
readFn.mockResolvedValue(null);
});

it("displays an error", async () => {
plainRender(<FileViewer file={file_name} title={title} />);
const dialog = await screen.findByRole("dialog");

within(dialog).getByText(/cannot read the file/i);
});
});

describe("when the file cannot be read", () => {
beforeEach(() => {
readFn.mockRejectedValue(new Error("read error"));
});

it("displays the error message", async () => {
plainRender(<FileViewer file={file_name} title={title} />);
const dialog = await screen.findByRole("dialog");

within(dialog).getByText(/read error/i);
});
});
});
67 changes: 67 additions & 0 deletions web/src/components/core/ShowLogButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) [2023] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of version 2 of the GNU General Public License as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import React, { useState } from "react";
import { FileViewer } from "~/components/core";
import { Icon } from "~/components/layout";
import { Button } from "@patternfly/react-core";

/**
* Button for displaying the YaST logs
*
* @component
*
* @param {function} onClickCallback callback triggered after clicking the button
*/
const ShowLogButton = ({ onClickCallback }) => {
const [isLogDisplayed, setIsLogDisplayed] = useState(false);

const onClick = () => {
if (onClickCallback) onClickCallback();
setIsLogDisplayed(true);
};

const onClose = () => {
setIsLogDisplayed(false);
};

return (
<>
<Button
variant="link"
onClick={onClick}
isDisabled={isLogDisplayed}
icon={<Icon name="description" size="24" />}
>
Show Logs
</Button>

{ isLogDisplayed &&
<FileViewer
title="YaST Logs"
file="/var/log/YaST2/y2log"
onCloseCallback={onClose}
/> }
</>
);
};

export default ShowLogButton;
44 changes: 44 additions & 0 deletions web/src/components/core/ShowLogButton.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) [2023] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of version 2 of the GNU General Public License as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import React from "react";
import { screen } from "@testing-library/react";
import { plainRender, mockComponent } from "~/test-utils";
import { ShowLogButton } from "~/components/core";

jest.mock("~/components/core/FileViewer", () => mockComponent("FileViewer Mock"));

describe("ShowLogButton", () => {
it("renders a button for displaying logs", () => {
plainRender(<ShowLogButton />);
const button = screen.getByRole("button", "Show Logs");
expect(button).not.toHaveAttribute("disabled");
});

describe("when user clicks on it", () => {
it("displays the FileView component", async () => {
const { user } = plainRender(<ShowLogButton />);
const button = screen.getByRole("button", "Show Logs");
await user.click(button);
screen.getByText(/FileViewer Mock/);
});
});
});
3 changes: 2 additions & 1 deletion web/src/components/core/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import React, { useState } from "react";
import { Icon, PageActions } from "~/components/layout";
import { About, ChangeProductButton, LogsButton } from "~/components/core";
import { About, ChangeProductButton, LogsButton, ShowLogButton } from "~/components/core";
import { TargetIpsPopup } from "~/components/network";

/**
Expand Down Expand Up @@ -64,6 +64,7 @@ export default function Sidebar() {
<About onClickCallback={close} />
<TargetIpsPopup onClickCallback={close} />
<LogsButton />
<ShowLogButton onClickCallback={close} />
</div>

<footer className="split" data-state="reversed">
Expand Down
2 changes: 2 additions & 0 deletions web/src/components/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export { default as InstallButton } from "./InstallButton";
export { default as InstallerSkeleton } from "./InstallerSkeleton";
export { default as KebabMenu } from "./KebabMenu";
export { default as LogsButton } from "./LogsButton";
export { default as FileViewer } from "./FileViewer";
export { default as ShowLogButton } from "./ShowLogButton";
export { default as PasswordAndConfirmationInput } from "./PasswordAndConfirmationInput";
export { default as Popup } from "./Popup";
export { default as ProgressReport } from "./ProgressReport";
Expand Down
2 changes: 2 additions & 0 deletions web/src/components/layout/Icon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ import Delete from "@icons/delete.svg?component";
import Warning from "@icons/warning.svg?component";
import Apps from "@icons/apps.svg?component";
import Loading from "./three-dots-loader-icon.svg?component";
import Description from "@icons/description.svg?component";

const icons = {
apps: Apps,
check_circle: CheckCircle,
delete: Delete,
description: Description,
download: Download,
downloading: Downloading,
edit: Edit,
Expand Down

0 comments on commit f605a5d

Please sign in to comment.