debian-image-builder-frontend/src/Components/Blueprints/BuildImagesButton.tsx
Gianluca Zuccarelli e8d46dd716 deps: migrate fec/notifications
The frontend component library decoupled notifications from redux.
Dispatching notifications via the notifications middleware was replaced
by a new `useAddNotifications` hook.

We mostly used the notifications middleware outside of React Components
in our `enhancedImageBuilderApi` store for mutation events. I created a
wrapper around the RTK hooks that uses the `useAddNotification` hook
and created a directory for the new hooks.

In other places, where we were using the notification dispatcher inside
React components, I replaced the call with the new hook.

[1] b1d4973144/packages/notifications/doc/migration.md

bump @redhat-cloud-services/frontend-components-notifications

---
updated-dependencies:
- dependency-name: "@redhat-cloud-services/frontend-components-notifications"
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Co-authored-by: dependabot[bot] <support@github.com>
Assisted-by: cursor ide for generalizing the `useMutationWithNotification`
hook.
2025-07-01 09:17:38 +00:00

186 lines
5.7 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
Dropdown,
MenuToggle,
Menu,
MenuContent,
MenuList,
MenuItem,
Flex,
FlexItem,
Spinner,
MenuToggleAction,
ButtonProps,
Button,
} 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);
})();
}, [auth]);
const onBuildHandler = async () => {
if (selectedBlueprintId) {
await buildBlueprint({
id: selectedBlueprintId,
body: {
image_types: blueprintImageType?.filter(
(target) => !deselectedTargets.includes(target)
),
},
});
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>
);
};