Skip to content
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
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