/* * Copyright (C) 2022 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from "cockpit"; import React, { useContext, useEffect, useRef, useState } from "react"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Content } from "@patternfly/react-core/dist/esm/components/Content/index.js"; import { ProgressStep, ProgressStepper } from "@patternfly/react-core/dist/esm/components/ProgressStepper/index.js"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { CheckCircleIcon } from "@patternfly/react-icons/dist/esm/icons/check-circle-icon"; import { ExclamationCircleIcon } from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon"; import { InProgressIcon } from "@patternfly/react-icons/dist/esm/icons/in-progress-icon"; import { PendingIcon } from "@patternfly/react-icons/dist/esm/icons/pending-icon"; import { BossClient, getActiveInstallationTask, getSteps, INSTALLATION_STATUS, installWithTasks } from "../../apis/boss.js"; import { exitGui, rebootSystem } from "../../helpers/exit.js"; import { debug } from "../../helpers/log.js"; import { BossContext, OsReleaseContext, SystemTypeContext } from "../../contexts/Common.jsx"; import { EmptyStatePanel } from "cockpit-components-empty-state.jsx"; import { Feedback } from "./Feedback.jsx"; import { InstallationNonCriticalErrorDialog } from "./InstallationNonCriticalErrorDialog.jsx"; import { useAutoReboot } from "./useAutoReboot.js"; import "./InstallationProgress.scss"; const _ = cockpit.gettext; const N_ = cockpit.noop; const SCREEN_ID = "anaconda-screen-progress"; const DETAIL_TYPE_YESNO = "yesno"; const PROGRESS_STEPS_DONE = 4; const progressStepsMap = { BOOTLOADER_INSTALLATION: 2, ENVIRONMENT_CONFIGURATION: 0, FINALIZATION: 3, SOFTWARE_INSTALLATION: 1, STORAGE_CONFIGURATION: 0, SYSTEM_CONFIGURATION: 2, }; export const InstallationProgress = ({ automatedInstall, onCritFail }) => { const [status, setStatus] = useState(); const [statusMessage, setStatusMessage] = useState(""); const [steps, setSteps] = useState(); const [currentProgressStep, setCurrentProgressStep] = useState(0); const [errorDialogData, setErrorDialogData] = useState(null); const refStatusMessage = useRef(""); const isBootIso = useContext(SystemTypeContext).systemType === "BOOT_ISO"; const osRelease = useContext(OsReleaseContext); const { installationStatus, pendingError } = useContext(BossContext); useAutoReboot(status, automatedInstall); useEffect(() => { const failureCtx = { context: _("Installation of the system failed") }; const connectToTask = (taskPath, shouldStart) => { const taskProxy = new BossClient().client.proxy( "org.fedoraproject.Anaconda.Task", taskPath ); const categoryProxy = new BossClient().client.proxy( "org.fedoraproject.Anaconda.TaskCategory", taskPath ); let lastStep = null; const addEventListeners = () => { taskProxy.addEventListener("ProgressChanged", (_, step, message) => { if (step === 0 && lastStep !== 0) { getSteps({ task: taskPath }) .then( ret => setSteps(ret.v), onCritFail() ); } lastStep = step; if (message && message !== refStatusMessage.current) { debug("ProgressChanged:", message); setStatusMessage(message); refStatusMessage.current = message; } }); taskProxy.addEventListener("Failed", () => { debug("Installation task Failed signal received"); setStatus("danger"); }); taskProxy.addEventListener("Stopped", () => { debug("Installation task Stopped signal received"); taskProxy.Finish().catch(onCritFail({ context: cockpit.format(N_("Installation of the system failed: $0"), refStatusMessage.current), })); }); categoryProxy.addEventListener("changed", (_, data) => { if ("CurrentCategory" in data) { debug("CategoryChanged:", data.CurrentCategory); const step = progressStepsMap[data.CurrentCategory]; setCurrentProgressStep(current => { if (step !== undefined && step >= current) { return step; } return current; }); } }); categoryProxy.addEventListener("ErrorRaised", (_, message, detailType) => { handleError(message, detailType, categoryProxy); }); taskProxy.addEventListener("Succeeded", () => { debug("Installation task Succeeded signal received"); setStatus("success"); setCurrentProgressStep(PROGRESS_STEPS_DONE); }); }; Promise.all([taskProxy.wait(), categoryProxy.wait()]).then(() => { addEventListeners(); if (shouldStart) { taskProxy.Start().catch(onCritFail(failureCtx)); } else { const step = progressStepsMap[categoryProxy.CurrentCategory]; if (step !== undefined) { setCurrentProgressStep(step); } getSteps({ task: taskPath }) .then( ret => setSteps(ret.v), onCritFail() ); handleError(pendingError.message, pendingError.type, categoryProxy); } }); }; const startNewInstallation = () => installWithTasks().then( tasks => connectToTask(tasks[0], true), onCritFail(failureCtx) ); const handleError = (message, detailType, categoryProxy) => { if (!message) return; if (detailType === DETAIL_TYPE_YESNO && categoryProxy) { setErrorDialogData({ categoryProxy, message }); } else { setStatus("danger"); categoryProxy?.RespondToError(false); onCritFail()({ message }); } }; /** Sync UI with the backend installation status: finalize if done, reconnect if running, or start a new installation. */ const syncInstallState = async () => { debug("syncInstallState: installationStatus=", installationStatus); switch (installationStatus) { case INSTALLATION_STATUS.SUCCEEDED: setStatus("success"); setCurrentProgressStep(PROGRESS_STEPS_DONE); setSteps([]); break; case INSTALLATION_STATUS.FAILED: handleError(pendingError.message, pendingError.type, null); break; case INSTALLATION_STATUS.RUNNING: try { const activeTask = await getActiveInstallationTask(); if (activeTask) { connectToTask(activeTask, false); } else { // this should be impossible. How is the status = RUNNING and no task is active? const errMsg = "Installation status is RUNNING but no active installation task was found"; debug(errMsg); throw new Error(errMsg); } } catch (error) { onCritFail(failureCtx)(error); } break; default: startNewInstallation(); } }; syncInstallState(); // eslint-disable-next-line react-hooks/exhaustive-deps -- pendingError is read on reconnection only, not reactively }, [installationStatus, onCritFail]); const submitErrorDecision = (shouldContinue) => { if (!errorDialogData) { return; } errorDialogData.categoryProxy.RespondToError(shouldContinue) .finally(() => { setErrorDialogData(null); }); }; if (steps === undefined) { return null; } let icon; let title; if (status === "success") { icon = CheckCircleIcon; title = _("Successfully installed"); } else if (status === "danger") { icon = ExclamationCircleIcon; title = _("Installation failed"); } else { title = _("Installing"); } const progressSteps = [ { description: _("Storage configuration: Storage is currently being configured."), id: SCREEN_ID + "-step-storage", title: _("Storage configuration"), }, { description: _("Software installation: Storage configuration complete. The software is now being installed onto your device."), id: SCREEN_ID + "-step-payload", title: _("Software installation"), }, { description: _("System configuration: Software installation complete. The system is now being configured."), id: SCREEN_ID + "-step-configuration", title: _("System configuration"), }, { description: _("Finalizing: The system configuration is complete. Finalizing installation may take a few moments."), id: SCREEN_ID + "-step-boot-loader", title: _("Finalization"), }, ]; const successActions = [ , ...(!isBootIso ? [] : []), ]; return ( {currentProgressStep < PROGRESS_STEPS_DONE ? progressSteps[currentProgressStep].description : cockpit.format(_("To begin using $0, reboot your system."), osRelease.PRETTY_NAME)} {currentProgressStep < PROGRESS_STEPS_DONE && ( <> {progressSteps.map((progressStep, index) => { let variant = "pending"; let ariaLabel = _("pending step"); let phaseText = _("Pending"); let statusText = ""; let phaseIcon = ; if (index < currentProgressStep) { variant = "success"; ariaLabel = _("completed step"); phaseText = _("Completed"); phaseIcon = ; } else if (index === currentProgressStep) { variant = status === "danger" ? status : "info"; ariaLabel = _("current step"); phaseText = _("In progress"); statusText = statusMessage; if (status === "danger") { phaseIcon = ; } else { phaseIcon = ; } } return ( {phaseText} {statusText} } > {progressStep.title} ); })} )} } secondary={status === "success" && successActions} title={title} headingLevel="h2" /> {(status === "success" || status === "danger") && } ); };