Skip to content

Commit

Permalink
Re-implement 'Modify storage' target to open cockpit storage in-page
Browse files Browse the repository at this point in the history
  • Loading branch information
KKoukiou committed Jan 18, 2024
1 parent 8ad3c71 commit c6f8b87
Show file tree
Hide file tree
Showing 13 changed files with 685 additions and 226 deletions.
131 changes: 125 additions & 6 deletions src/components/AnacondaWizard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import {

import { AnacondaPage } from "./AnacondaPage.jsx";
import { InstallationMethod, getPageProps as getInstallationMethodProps } from "./storage/InstallationMethod.jsx";
import { getDefaultScenario } from "./storage/InstallationScenario.jsx";
import { getDefaultScenario, scenarios, AvailabilityState } from "./storage/InstallationScenario.jsx";
import { CockpitStorageIntegration } from "./storage/CockpitStorageIntegration.jsx";
import { MountPointMapping, getPageProps as getMountPointMappingProps } from "./storage/MountPointMapping.jsx";
import { DiskEncryption, getStorageEncryptionState, getPageProps as getDiskEncryptionProps } from "./storage/DiskEncryption.jsx";
import { InstallationLanguage, getPageProps as getInstallationLanguageProps } from "./localization/InstallationLanguage.jsx";
Expand All @@ -44,7 +45,15 @@ import { ReviewConfiguration, ReviewConfigurationConfirmModal, getPageProps as g
import { exitGui } from "../helpers/exit.js";
import {
getMountPointConstraints,
getDevices,
getDiskFreeSpace,
getDiskTotalSpace,
getRequiredDeviceSize,
} from "../apis/storage_devicetree.js";
import {
getRequiredSpace
} from "../apis/payloads.js";
import { findDuplicatesInArray } from "../helpers/utils.js";
import {
applyStorage,
resetPartitioning,
Expand All @@ -54,10 +63,11 @@ import { SystemTypeContext, OsReleaseContext } from "./Common.jsx";
const _ = cockpit.gettext;
const N_ = cockpit.noop;

export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtimeData, onCritFail, title, conf }) => {
export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtimeData, onCritFail, showStorage, setShowStorage, title, conf }) => {
const [isFormDisabled, setIsFormDisabled] = useState(false);
const [isFormValid, setIsFormValid] = useState(false);
const [mountPointConstraints, setMountPointConstraints] = useState();
const [requiredSize, setRequiredSize] = useState();
const [reusePartitioning, setReusePartitioning] = useState(false);
const [stepNotification, setStepNotification] = useState();
const [storageEncryption, setStorageEncryption] = useState(getStorageEncryptionState());
Expand All @@ -67,6 +77,77 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
const [currentStepId, setCurrentStepId] = useState();
const osRelease = useContext(OsReleaseContext);
const isBootIso = useContext(SystemTypeContext) === "BOOT_ISO";
const [scenarioAvailability, setScenarioAvailability] = useState(Object.fromEntries(
scenarios.map((s) => [s.id, new AvailabilityState()])
));
const [diskTotalSpace, setDiskTotalSpace] = useState();
const [diskFreeSpace, setDiskFreeSpace] = useState();
const [hasFilesystems, setHasFilesystems] = useState();
const [duplicateDeviceNames, setDuplicateDeviceNames] = useState([]);
const selectedDisks = storageData.diskSelection.selectedDisks;

useEffect(() => {
const updateSizes = async () => {
const diskTotalSpace = await getDiskTotalSpace({ diskNames: selectedDisks }).catch(console.error);
const diskFreeSpace = await getDiskFreeSpace({ diskNames: selectedDisks }).catch(console.error);
const devices = await getDevices().catch(console.error);
const _duplicateDeviceNames = findDuplicatesInArray(devices);

setDuplicateDeviceNames(_duplicateDeviceNames);
setDiskTotalSpace(diskTotalSpace);
setDiskFreeSpace(diskFreeSpace);
};
updateSizes();
}, [selectedDisks]);

useEffect(() => {
if ([diskTotalSpace, diskFreeSpace, hasFilesystems, mountPointConstraints, requiredSize].some(itm => itm === undefined)) {
return;
}

setScenarioAvailability(oldAvailability => {
const newAvailability = {};

for (const scenario of scenarios) {
const availability = scenario.check({
oldPartitioning: oldAvailability[scenario.id].partitioning,
diskFreeSpace,
diskTotalSpace,
duplicateDeviceNames,
partitioning: storageData.partitioning.path,
hasFilesystems,
requiredSize,
storageScenarioId,
});
newAvailability[scenario.id] = availability;
}
return newAvailability;
});
}, [
diskFreeSpace,
diskTotalSpace,
duplicateDeviceNames,
hasFilesystems,
mountPointConstraints,
requiredSize,
storageData.partitioning.path,
storageData.devices,
storageScenarioId,
]);

useEffect(() => {
getDevices().then(res => {
const _duplicateDeviceNames = findDuplicatesInArray(res);
setDuplicateDeviceNames(_duplicateDeviceNames);
setIsFormValid(_duplicateDeviceNames.length === 0);
}, onCritFail({ context: N_("Failed to get device names.") }));
}, [storageData.devices, onCritFail, setIsFormValid]);

useEffect(() => {
const hasFilesystems = selectedDisks.some(device => storageData.devices[device]?.children.v.some(child => storageData.devices[child]?.formatData.mountable.v || storageData.devices[child]?.formatData.type.v === "luks"));

setHasFilesystems(hasFilesystems);
}, [selectedDisks, storageData.devices]);

const availableDevices = useMemo(() => {
return Object.keys(storageData.devices);
Expand All @@ -80,6 +161,16 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
updateMountPointConstraints();
}, []);

useEffect(() => {
const updateRequiredSize = async () => {
const requiredSpace = await getRequiredSpace().catch(console.error);
const requiredSize = await getRequiredDeviceSize({ requiredSpace }).catch(console.error);

setRequiredSize(requiredSize);
};
updateRequiredSize();
}, []);

useEffect(() => {
if (!currentStepId) {
return;
Expand All @@ -94,7 +185,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
* but for custom mount assignment we try to reuse the partitioning when possible.
*/
setReusePartitioning(false);
}, [availableDevices, storageData.diskSelection.selectedDisks]);
}, [availableDevices, selectedDisks]);

const language = useMemo(() => {
for (const l of Object.keys(localizationData.languages)) {
Expand All @@ -105,6 +196,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
}
}
}, [localizationData]);

const stepsOrder = [
{
component: InstallationLanguage,
Expand All @@ -116,12 +208,17 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
data: {
deviceData: storageData.devices,
diskSelection: storageData.diskSelection,
// HACK - is there a more official source for this?
isEfi: mountPointConstraints?.some(c => c["required-filesystem-type"]?.v === "efi"),
dispatch,
requiredSize,
scenarioAvailability,
storageScenarioId,
setStorageScenarioId: (scenarioId) => {
window.sessionStorage.setItem("storage-scenario-id", scenarioId);
setStorageScenarioId(scenarioId);
}
},
setShowStorage,
},
...getInstallationMethodProps({ isBootIso, osRelease, isFormValid })
},
Expand Down Expand Up @@ -273,14 +370,36 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
);
}

const firstVisibleStepIndex = steps.findIndex(step => !step.props.isHidden) + 1;
const startIndex = steps.findIndex(step => {
// Find the first step that is not hidden if the Wizard is opening for the first time.
// Otherwise, find the first step that was last visited.
return currentStepId ? step.props.id === currentStepId : !step.props.isHidden;
}) + 1;

if (showStorage) {
return (
<CockpitStorageIntegration
deviceData={storageData.devices}
selectedDisks={storageData.diskSelection}
dispatch={dispatch}
mountPointConstraints={mountPointConstraints}
onCritFail={onCritFail}
requiredSize={requiredSize}
scenarioAvailability={scenarioAvailability}
setScenarioAvailability={setScenarioAvailability}
setStorageScenarioId={setStorageScenarioId}
setShowStorage={setShowStorage} />
);
}

console.info({ storageScenarioId, part: storageData.partitioning });

return (
<PageSection type={PageSectionTypes.wizard} variant={PageSectionVariants.light}>
<Wizard
id="installation-wizard"
isVisitRequired
startIndex={firstVisibleStepIndex}
startIndex={startIndex}
footer={<Footer
onCritFail={onCritFail}
isFormValid={isFormValid}
Expand Down
1 change: 1 addition & 0 deletions src/components/Common.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const ConfContext = createContext();
export const LanguageContext = createContext("");
export const SystemTypeContext = createContext(null);
export const OsReleaseContext = createContext(null);
export const TargetSystemRootContext = createContext(null);

export const FormGroupHelpPopover = ({ helpContent }) => {
return (
Expand Down
34 changes: 20 additions & 14 deletions src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
import { read_os_release as readOsRelease } from "os-release.js";

import { WithDialogs } from "dialogs.jsx";
import { AddressContext, LanguageContext, SystemTypeContext, OsReleaseContext } from "./Common.jsx";
import { AddressContext, LanguageContext, SystemTypeContext, TargetSystemRootContext, OsReleaseContext } from "./Common.jsx";
import { AnacondaHeader } from "./AnacondaHeader.jsx";
import { AnacondaWizard } from "./AnacondaWizard.jsx";
import { CriticalError, errorHandlerWithContext, bugzillaPrefiledReportURL } from "./Error.jsx";
Expand Down Expand Up @@ -56,6 +56,7 @@ export const Application = () => {
const [storeInitilized, setStoreInitialized] = useState(false);
const criticalError = state?.error?.criticalError;
const [jsError, setJsEroor] = useState();
const [showStorage, setShowStorage] = useState(false);

const onCritFail = useCallback((contextData) => {
return errorHandlerWithContext(contextData, exc => dispatch(setCriticalErrorAction(exc)));
Expand Down Expand Up @@ -136,27 +137,32 @@ export const Application = () => {
reportLinkURL={bzReportURL} />}
{!jsError &&
<>
{!showStorage &&
<PageGroup stickyOnBreakpoint={{ default: "top" }}>
<AnacondaHeader
title={title}
reportLinkURL={bzReportURL}
isConnected={state.network.connected}
onCritFail={onCritFail}
/>
</PageGroup>
</PageGroup>}
<AddressContext.Provider value={address}>
<WithDialogs>
<AnacondaWizard
onCritFail={onCritFail}
title={title}
storageData={state.storage}
localizationData={state.localization}
runtimeData={state.runtime}
dispatch={dispatch}
conf={conf}
osRelease={osRelease}
/>
</WithDialogs>
<TargetSystemRootContext.Provider value={conf["Installation Target"].system_root}>
<WithDialogs>
<AnacondaWizard
onCritFail={onCritFail}
title={title}
storageData={state.storage}
localizationData={state.localization}
runtimeData={state.runtime}
dispatch={dispatch}
conf={conf}
osRelease={osRelease}
setShowStorage={setShowStorage}
showStorage={showStorage}
/>
</WithDialogs>
</TargetSystemRootContext.Provider>
</AddressContext.Provider>
</>}
</Page>
Expand Down
9 changes: 8 additions & 1 deletion src/components/review/ReviewConfiguration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,14 @@ export const ReviewConfiguration = ({ deviceData, diskSelection, language, local
<DescriptionListDescription id={idPrefix + "-target-storage"}>
<Stack hasGutter>
{diskSelection.selectedDisks.map(disk => {
return <DeviceRow key={disk} deviceData={deviceData} disk={disk} requests={storageScenarioId === "mount-point-mapping" ? requests : null} />;
return (
<DeviceRow
key={disk}
deviceData={deviceData}
disk={disk}
requests={["mount-point-mapping", "use-configured-storage"].includes(storageScenarioId) ? requests : null}
/>
);
})}
</Stack>
</DescriptionListDescription>
Expand Down

0 comments on commit c6f8b87

Please sign in to comment.