* Copyright (C) 2021 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Page, PageGroup, PageSection, PageSectionTypes } from "@patternfly/react-core/dist/esm/components/Page/index.js";
import { BossClient } from "../apis/boss.js";
import { initialState, reducer, useReducerWithThunk } from "../reducer.js";
import { getInstallerConfValue, parseAnacondaConfBool, readConf } from "../helpers/conf.js";
import { isExiting } from "../helpers/exit.js";
import { debug } from "../helpers/log.js";
import { getAnacondaUIVersion, getAnacondaVersion } from "../helpers/product.js";
import { MainContextWrapper } from "../contexts/Common.jsx";
import { EmptyStatePanel } from "cockpit-components-empty-state";
import { read_os_release as readOsRelease } from "os-release.js";
import { AnacondaHeader } from "./AnacondaHeader.jsx";
import { AnacondaWizard } from "./AnacondaWizard.jsx";
import { ErrorBoundary } from "./Error.jsx";
const _ = cockpit.gettext;
const N_ = cockpit.noop;
export const ApplicationLoading = () => (
<PageSection className="installation-page--loading" hasBodyWrapper={false} type={PageSectionTypes.wizard}>
<EmptyStatePanel loading title={_("Initializing...")} />
</PageSection>
);
const ApplicationCompleted = () => (
<PageSection className="installation-page--loading" hasBodyWrapper={false} type={PageSectionTypes.wizard}>
<EmptyStatePanel
title={_("Installation completed")}
paragraph={_("You can close this browser window now.")}
/>
</PageSection>
);
export const Application = ({ conf, dispatch, installationStatus, isFetching, onCritFail, osRelease, reportLinkURL, setShowStorage, showStorage }) => {
const [storeInitialized, setStoreInitialized] = useState(false);
const [currentStepId, setCurrentStepId] = useState();
const address = useAddress(onCritFail);
const automatedInstall = parseAnacondaConfBool(getInstallerConfValue(conf, "Runtime", "automated_install"));
const pauseAtSummary = parseAnacondaConfBool(getInstallerConfValue(conf, "Runtime", "pause_at_summary"));
useEffect(() => {
if (!address) {
return;
}
const preventExit = () => {
return "";
};
window.addEventListener("beforeunload", preventExit);
const allowExternalNavigation = (event) => {
const link = event.target.closest("a");
if (link && (link.href.startsWith("extlink:") || link.href.startsWith("anaconda-gnome-control-center:"))) {
window.removeEventListener("beforeunload", preventExit);
setTimeout(() => {
window.addEventListener("beforeunload", preventExit);
}, 1000);
}
};
document.addEventListener("click", allowExternalNavigation);
new BossClient(address, dispatch).init({ automatedInstall, conf })
.then(() => {
setStoreInitialized(true);
}, onCritFail({ context: N_("Reading information about the computer failed.") }));
}, [address, automatedInstall, conf, dispatch, onCritFail]);
if (!address || !storeInitialized || !installationStatus) {
if (isExiting()) {
return <ApplicationCompleted />;
}
debug("Loading initial data...");
return <ApplicationLoading />;
}
const title = cockpit.format(_("$0 installation"), osRelease.PRETTY_NAME);
return (
<>
<PageGroup
isFilled={false}
stickyOnBreakpoint={{ default: "top" }}>
<AnacondaHeader
dispatch={dispatch}
title={title}
reportLinkURL={reportLinkURL}
onCritFail={onCritFail}
showStorage={showStorage}
setShowStorage={setShowStorage}
/>
</PageGroup>
<AnacondaWizard
automatedInstall={automatedInstall}
currentStepId={currentStepId}
isFetching={isFetching}
onCritFail={onCritFail}
pauseAtSummary={pauseAtSummary}
title={title}
dispatch={dispatch}
setCurrentStepId={setCurrentStepId}
setShowStorage={setShowStorage}
showStorage={showStorage}
/>
</>
);
};
const useConf = ({ onCritFail }) => {
const [conf, setConf] = useState();
useEffect(() => {
readConf().then(setConf, onCritFail({ context: N_("Reading installer configuration failed.") }));
}, [onCritFail]);
return conf;
};
const useOsRelease = ({ onCritFail }) => {
const [osRelease, setOsRelease] = useState();
useEffect(() => {
readOsRelease().then(setOsRelease, onCritFail({ context: N_("Reading information about the OS failed.") }));
}, [onCritFail]);
return osRelease;
};
const useAddress = (onCritFail) => {
const [backendReady, setBackendReady] = useState(false);
const [address, setAddress] = useState();
useEffect(() => {
if (!backendReady) {
return;
}
cockpit.file("/run/anaconda/bus.address").watch(address => {
setAddress(address);
});
}, [backendReady]);
const wasReadyRef = useRef(false);
useEffect(() => {
cockpit.file("/run/anaconda/backend_ready").watch(
(content) => {
const isReady = content !== null;
if (isReady) {
wasReadyRef.current = true;
}
setBackendReady(isReady);
if (!isReady && wasReadyRef.current && onCritFail && !isExiting()) {
onCritFail()({
message: _("The Anaconda installation has stopped unexpectedly."),
});
}
}
);
}, [onCritFail]);
return address;
};
const useAppVersion = () => {
const initialState = {
webui: getAnacondaUIVersion(),
};
const [appVersion, setAppVersion] = useState(initialState);
useEffect(() => {
getAnacondaVersion().then(value => setAppVersion(obj => ({ ...obj, backend: value })));
}, []);
return appVersion;
};
export const ApplicationWithErrorBoundary = () => {
const [showStorage, setShowStorage] = useState(false);
const [state, dispatch] = useReducerWithThunk(reducer, initialState);
const [errorBeforeBoundary, setErrorBeforeBoundary] = useState();
const onCritFail = useCallback(
(context) => (exc) => setErrorBeforeBoundary({ contextData: { context }, ...exc }),
[]
);
const conf = useConf({ onCritFail });
const osRelease = useOsRelease({ onCritFail });
const appVersion = useAppVersion();
if (!conf || !osRelease) {
return <ApplicationLoading />;
}
return (
<MainContextWrapper state={state} osRelease={osRelease} conf={conf} appVersion={appVersion}>
<Page className="no-masthead-sidebar" data-debug={conf.Anaconda.debug}>
<ErrorBoundary
backendException={errorBeforeBoundary}
showStorage={showStorage}
>
<Application
conf={conf}
dispatch={dispatch}
installationStatus={state.boss.installationStatus}
isFetching={state.misc.isFetching}
onCritFail={onCritFail}
osRelease={osRelease}
showStorage={showStorage}
setShowStorage={setShowStorage}
/>
</ErrorBoundary>
</Page>
</MainContextWrapper>
);
};