debian-image-builder-frontend/src/Components/Blueprints/BuildImagesButton.tsx
2025-08-05 13:56:59 +00:00

191 lines
5.9 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
Button,
ButtonProps,
Dropdown,
Flex,
FlexItem,
Menu,
MenuContent,
MenuItem,
MenuList,
MenuToggle,
MenuToggleAction,
Spinner,
} from '@patternfly/react-core';
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { skipToken } from '@reduxjs/toolkit/query';
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
import { useComposeBPWithNotification as useComposeBlueprintMutation } from '../../Hooks';
import { useGetBlueprintQuery } from '../../store/backendApi';
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
import { useAppSelector } from '../../store/hooks';
import { ImageTypes } from '../../store/imageBuilderApi';
type BuildImagesButtonPropTypes = {
// default children is 'Build images'
children?: React.ReactNode;
};
export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const [deselectedTargets, setDeselectedTargets] = useState<ImageTypes[]>([]);
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
useComposeBlueprintMutation();
const { analytics, auth } = useChrome();
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
useEffect(() => {
(async () => {
const data = await auth.getUser();
setUserData(data);
})();
// This useEffect hook should run *only* on mount and therefore has an empty
// dependency array. eslint's exhaustive-deps rule does not support this use.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onBuildHandler = async () => {
if (selectedBlueprintId) {
await buildBlueprint({
id: selectedBlueprintId,
body: {
image_types: blueprintImageType?.filter(
(target) => !deselectedTargets.includes(target),
),
},
});
if (!process.env.IS_ON_PREMISE) {
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Requested`, {
module: AMPLITUDE_MODULE_NAME,
trigger: 'synchronize images',
account_id: userData?.identity.internal?.account_id || 'Not found',
});
}
}
};
const [isOpen, setIsOpen] = useState(false);
const onToggleClick = () => {
setIsOpen(!isOpen);
};
const { data: blueprintDetails } = useGetBlueprintQuery(
selectedBlueprintId ? { id: selectedBlueprintId } : skipToken,
);
const blueprintImageType = blueprintDetails?.image_requests.map(
(image) => image.image_type,
);
const onSelect = (
_event: React.MouseEvent<Element, MouseEvent>,
itemId: number,
) => {
const imageType = blueprintImageType?.[itemId];
if (imageType && deselectedTargets.includes(imageType)) {
setDeselectedTargets(
deselectedTargets.filter((target) => target !== imageType),
);
} else if (imageType) {
setDeselectedTargets([...deselectedTargets, imageType]);
}
};
return (
<Dropdown
isOpen={isOpen}
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
variant='primary'
data-testid='blueprint-build-image-menu'
ref={toggleRef}
onClick={onToggleClick}
isExpanded={isOpen}
splitButtonItems={[
<MenuToggleAction
data-testid='blueprint-build-image-menu-option'
key='split-action'
onClick={onBuildHandler}
id='wizard-build-image-btn'
isDisabled={
!selectedBlueprintId ||
deselectedTargets.length === blueprintImageType?.length
}
>
<Flex display={{ default: 'inlineFlex' }}>
{imageBuildLoading && (
<FlexItem>
<Spinner
style={
{
'--pf-v6-c-spinner--Color': '#fff',
} as React.CSSProperties
}
isInline
size='md'
/>
</FlexItem>
)}
<FlexItem>{children ? children : 'Build images'}</FlexItem>
</Flex>
</MenuToggleAction>,
]}
></MenuToggle>
)}
>
<Menu onSelect={onSelect} selected={blueprintImageType}>
<MenuContent>
<MenuList>
{blueprintImageType?.map((imageType, index) => (
<MenuItem
key={imageType}
hasCheckbox
itemId={index}
isSelected={
deselectedTargets.length === 0 ||
!deselectedTargets.includes(imageType)
}
>
{targetOptions[imageType]}
</MenuItem>
))}
</MenuList>
</MenuContent>
</Menu>
</Dropdown>
);
};
type BuildImagesButtonEmptyStatePropTypes = {
variant?: ButtonProps['variant'];
children?: React.ReactNode;
};
export const BuildImagesButtonEmptyState = ({
variant,
children,
}: BuildImagesButtonEmptyStatePropTypes) => {
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
useComposeBlueprintMutation();
const onBuildHandler = async () => {
if (selectedBlueprintId) {
await buildBlueprint({ id: selectedBlueprintId, body: {} });
}
};
return (
<Button
onClick={onBuildHandler}
isDisabled={!selectedBlueprintId}
isLoading={imageBuildLoading}
variant={variant || 'primary'}
>
{children ? children : 'Build images'}
</Button>
);
};