Skip to content

Commit

Permalink
Web UI: Add a log viewer
Browse files Browse the repository at this point in the history
Add a log viewer that opens in a modal window when the
"Show logs" button in the upper right corner of the screen
is clicked.

The log viewer is currently hardcoded to follow /tmp/anaconda.log.
  • Loading branch information
M4rtinK committed Jun 10, 2022
1 parent 7265b5d commit 0f7d76f
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 6 deletions.
17 changes: 15 additions & 2 deletions ui/webui/src/components/AnacondaHeader.jsx
Expand Up @@ -19,7 +19,8 @@ import cockpit from "cockpit";
import React from "react";

import {
Flex,
Button,
Flex, FlexItem,
Label,
PageSection, PageSectionVariants,
Popover,
Expand All @@ -31,7 +32,7 @@ const _ = cockpit.gettext;

const prerelease = _("Pre-release");

export const AnacondaHeader = ({ beta, title }) => {
export const AnacondaHeader = ({ beta, title, setShowLogViewer }) => {
const betanag = beta
? (
<Popover
Expand Down Expand Up @@ -63,6 +64,18 @@ export const AnacondaHeader = ({ beta, title }) => {
<Text component="h1">{title}</Text>
</TextContent>
{betanag}
<FlexItem align={{ default: "alignRight" }}>
<Button
aria-label={_("Show log")}
id="global-show-log-btn"
variant="tertiary"
onClick={() => {
setShowLogViewer(true);
}}
>
{_("Show log")}
</Button>
</FlexItem>
</Flex>
</PageSection>
);
Expand Down
11 changes: 9 additions & 2 deletions ui/webui/src/components/AnacondaWizard.jsx
Expand Up @@ -33,12 +33,13 @@ import { InstallationDestination, applyDefaultStorage } from "./storage/Installa
import { InstallationLanguage } from "./localization/InstallationLanguage.jsx";
import { InstallationProgress } from "./installation/InstallationProgress.jsx";
import { ReviewConfiguration, ReviewConfigurationConfirmModal } from "./review/ReviewConfiguration.jsx";
import { LogViewerModal } from "./LogViewerModal.jsx";
import { exitGui } from "../helpers/exit.js";
import { usePageLocation } from "hooks";

const _ = cockpit.gettext;

export const AnacondaWizard = ({ onAddErrorNotification, title }) => {
export const AnacondaWizard = ({ onAddErrorNotification, title, showLogViewer, setShowLogViewer }) => {
const [isFormValid, setIsFormValid] = useState(true);
const [stepNotification, setStepNotification] = useState();
const [isInProgress, setIsInProgress] = useState(false);
Expand Down Expand Up @@ -103,6 +104,8 @@ export const AnacondaWizard = ({ onAddErrorNotification, title }) => {
setStepNotification={setStepNotification}
isInProgress={isInProgress}
setIsInProgress={setIsInProgress}
showLogViewer={showLogViewer}
setShowLogViewer={setShowLogViewer}
/>}
hideClose
mainAriaLabel={`${title} content`}
Expand All @@ -116,7 +119,7 @@ export const AnacondaWizard = ({ onAddErrorNotification, title }) => {
);
};

const Footer = ({ isFormValid, setIsFormValid, setStepNotification, isInProgress, setIsInProgress }) => {
const Footer = ({ isFormValid, setIsFormValid, setStepNotification, isInProgress, setIsInProgress, showLogViewer, setShowLogViewer }) => {
const [nextWaitsConfirmation, setNextWaitsConfirmation] = useState(false);
const [quitWaitsConfirmation, setQuitWaitsConfirmation] = useState(false);

Expand Down Expand Up @@ -179,6 +182,10 @@ const Footer = ({ isFormValid, setIsFormValid, setStepNotification, isInProgress
exitGui={exitGui}
setQuitWaitsConfirmation={setQuitWaitsConfirmation}
/>}
{showLogViewer &&
<LogViewerModal
setShowLogViewer={setShowLogViewer}
/>}
<ActionList>
<Button
id="installation-next-btn"
Expand Down
100 changes: 100 additions & 0 deletions ui/webui/src/components/LogViewerModal.jsx
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2022 Red Hat, Inc.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with This program; If not, see <http://www.gnu.org/licenses/>.
*/
import cockpit from "cockpit";

import React, { useEffect, useState } from "react";

import { LogViewer, LogViewerSearch } from "@patternfly/react-log-viewer";

import { watchLogFile } from "../helpers/logs.js";

import {
Button,
Modal, ModalVariant,
Toolbar, ToolbarContent, ToolbarItem,
Text, TextContent, TextVariants,
Flex,
} from "@patternfly/react-core";

const _ = cockpit.gettext;

const AnacondaLogViewer = () => {
const [logData, setLogData] = useState("");

useEffect(() => {
const appendLogData = (newLogData, dataTag, error) => {
setLogData(l => l + newLogData);
if (error) {
console.log("Log file read failed.");
console.log(error);
}
};
watchLogFile("/tmp/anaconda.log", appendLogData);
}, []);

return (
<LogViewer
// see CSS file for a PF workaround needed
// for line numbers to work correctly
hasLineNumbers
isTextWrapped={false}
height={450}
data={logData}
toolbar={
<Toolbar>
<ToolbarContent>
<ToolbarItem>
<LogViewerSearch placeholder={_("Search value")} />
</ToolbarItem>
</ToolbarContent>
</Toolbar>
}
/>
);
};

export const LogViewerModal = ({ setShowLogViewer }) => {
return (
<Modal
id="log-viewer-modal"
actions={[
<Button
id="log-viewer-exit-btn"
key="cancel"
onClick={() => {
setShowLogViewer(false);
}}
variant="primary">
{_("Close")}
</Button>
]}
isOpen
onClose={() => setShowLogViewer(false)}
title={_("Installer log")}
variant={ModalVariant.medium}
>
<Flex direction={{ default: "column" }}>
<TextContent>
<Text component={TextVariants.p}>
{_("Installer version") + " 37.10"}
</Text>
</TextContent>
<AnacondaLogViewer />
</Flex>
</Modal>
);
};
10 changes: 8 additions & 2 deletions ui/webui/src/components/app.jsx
Expand Up @@ -42,6 +42,7 @@ export const Application = () => {
const [beta, setBeta] = useState();
const [conf, setConf] = useState();
const [notifications, setNotifications] = useState({});
const [showLogViewer, setShowLogViewer] = useState(false);

useEffect(() => {
cockpit.file("/run/anaconda/bus.address").watch(address => {
Expand Down Expand Up @@ -90,7 +91,7 @@ export const Application = () => {
<Page
data-debug={conf.Anaconda.debug}
additionalGroupedContent={
<AnacondaHeader beta={beta} title={title} />
<AnacondaHeader beta={beta} title={title} setShowLogViewer={setShowLogViewer} />
}
groupProps={{
sticky: "top"
Expand Down Expand Up @@ -120,7 +121,12 @@ export const Application = () => {
})}
</AlertGroup>}
<AddressContext.Provider value={address}>
<AnacondaWizard onAddErrorNotification={onAddErrorNotification} title={title} />
<AnacondaWizard
onAddErrorNotification={onAddErrorNotification}
title={title}
showLogViewer={showLogViewer}
setShowLogViewer={setShowLogViewer}
/>
</AddressContext.Provider>
</Page>
);
Expand Down
7 changes: 7 additions & 0 deletions ui/webui/src/components/app.scss
Expand Up @@ -15,3 +15,10 @@ html:not(.index-page) body {
#installation-wizard .pf-c-wizard__main-body {
flex: 1 1 auto;
}

// workaround for a LogViewer bug
// - without this the line number column is too narrow
// and line numbers quickly become unreadable
.pf-c-log-viewer__index {
width: auto;
}
22 changes: 22 additions & 0 deletions ui/webui/src/helpers/logs.js
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2022 Red Hat, Inc.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with This program; If not, see <http://www.gnu.org/licenses/>.
*/
import cockpit from "cockpit";

export const watchLogFile = (logFilePath, callback) => {
const file = cockpit.file(logFilePath);
file.watch(callback);
};

0 comments on commit 0f7d76f

Please sign in to comment.