* Copyright (C) 2025 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import React, { useContext, useEffect, useMemo } from "react";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { getDefaultEnvironment, resolveEnvironment, setPackagesSelection } from "../../apis/payload_dnf.js";
import { PageContext, PayloadContext } from "../../contexts/Common.jsx";
import { MenuSearch } from "../common/MenuSearch.jsx";
import "./SoftwareSelection.scss";
const _ = cockpit.gettext;
const SCREEN_ID = "anaconda-screen-software-selection";
const EnvironmentSelection = () => {
const { environments, selection } = useContext(PayloadContext);
const environment = selection?.environment;
useEffect(() => {
(async () => {
if (!environment) {
const defaultEnv = await getDefaultEnvironment();
await setPackagesSelection({ environment: defaultEnv, groups: [] });
}
})();
}, [environment]);
const options = useMemo(() => {
if (!environments) {
return [];
}
return environments.map((env) => ({
id: `${SCREEN_ID}-environment-${env.id}`,
itemDescription: env.description,
itemId: env.id,
itemText: env.name,
itemType: "menu-item",
key: `environment-${env.id}`,
onSearch: (search) => {
const searchLower = search.toLowerCase();
return env.name.toLowerCase().includes(searchLower) ||
(env.description && env.description.toLowerCase().includes(searchLower));
},
}));
}, [environments]);
const handleOnSelect = (_ev, itemId) => {
setPackagesSelection({ environment: itemId, groups: [] });
};
return (
<FormGroup
className="anaconda-screen-software-selection-form-group"
label={_("Base environment")}
>
<MenuSearch
ariaLabelSearch={_("Search for an environment")}
handleOnSelect={handleOnSelect}
menuType="environment"
options={options}
screenId={SCREEN_ID}
selection={environment}
/>
</FormGroup>
);
};
const GroupPackagesSelection = () => {
const { groups, selection } = useContext(PayloadContext);
const selectedGroups = selection?.groups || [];
const groupOptions = useMemo(() => {
if (!groups) {
return [];
}
const optionalGroups = groups.filter(group => group.isOptional);
const visibleGroups = groups.filter(group => !group.isOptional);
const optionalGroupItems = optionalGroups.map((group) => ({
id: `${SCREEN_ID}-group-${group.id}`,
itemDescription: group.description,
itemId: group.id,
itemText: group.name,
itemType: "menu-item",
key: `group-${group.id}`,
onSearch: (search) => {
const searchLower = search.toLowerCase();
return group.name.toLowerCase().includes(searchLower) ||
(group.description && group.description.toLowerCase().includes(searchLower));
},
}));
const visibleGroupItems = visibleGroups.map((group) => ({
id: `${SCREEN_ID}-group-${group.id}`,
itemDescription: group.description,
itemId: group.id,
itemText: group.name,
itemType: "menu-item",
key: `group-${group.id}`,
onSearch: (search) => {
const searchLower = search.toLowerCase();
return group.name.toLowerCase().includes(searchLower) ||
(group.description && group.description.toLowerCase().includes(searchLower));
},
}));
const options = [];
if (optionalGroupItems.length > 0) {
options.push({
id: `${SCREEN_ID}-optional-groups`,
itemChildren: optionalGroupItems,
itemLabel: _("Add-ons for your chosen environment"),
itemType: "menu-group",
key: "optional-groups",
});
}
if (visibleGroupItems.length > 0) {
options.push({
id: `${SCREEN_ID}-visible-groups`,
itemChildren: visibleGroupItems,
itemLabel: _("Add-ons not specific to your environment"),
itemType: "menu-group",
key: "visible-groups",
});
}
return options;
}, [groups]);
const handleGroupSelect = async (_ev, groupId) => {
let newSelectedGroups;
if (selectedGroups.includes(groupId)) {
newSelectedGroups = selectedGroups.filter(id => id !== groupId);
} else {
newSelectedGroups = [...selectedGroups, groupId];
}
await setPackagesSelection({ groups: newSelectedGroups });
};
return (
<FormGroup
className="anaconda-screen-software-selection-form-group"
label={_("Additional software for the selected environment")}
>
<MenuSearch
ariaLabelSearch={_("Search for additional software")}
handleOnSelect={handleGroupSelect}
menuType="groups"
options={groupOptions}
screenId={SCREEN_ID}
selection={selectedGroups}
/>
</FormGroup>
);
};
export const SoftwareSelection = ({ automatedInstall }) => {
const { setIsFormValid } = useContext(PageContext) ?? {};
const { packagesKickstarted, selection } = useContext(PayloadContext);
const environment = selection?.environment;
* Match pyanaconda.ui.lib.software.is_software_selection_complete (and GTK spoke `completed`).
* kickstarted=True in Anaconda means flags.automatedInstall and payload PackagesKickstarted.
*/
useEffect(() => {
const kickstarted =
packagesKickstarted === true && automatedInstall === true;
(async () => {
if (kickstarted && !environment) {
setIsFormValid(true);
return;
}
if (!environment) {
setIsFormValid(false);
return;
}
const resolved = await resolveEnvironment(environment);
setIsFormValid(Boolean(resolved));
})();
}, [environment, packagesKickstarted, automatedInstall, setIsFormValid]);
return (
<Form>
<Flex spaceItems={{ default: "spaceItemsXl" }}>
<EnvironmentSelection />
<GroupPackagesSelection />
</Flex>
</Form>
);
};