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.
This commit is contained in:
Gianluca Zuccarelli 2025-06-24 16:45:14 +01:00 committed by Klara Simickova
parent 77e0f5d6bf
commit e8d46dd716
32 changed files with 412 additions and 473 deletions

View file

@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import NotificationsPortal from '@redhat-cloud-services/frontend-components-notifications/NotificationPortal';
import NotificationsProvider from '@redhat-cloud-services/frontend-components-notifications/NotificationsProvider';
import '@patternfly/patternfly/patternfly-addons.css';
import { Router } from './Router';
@ -26,8 +26,9 @@ const App = () => {
return (
<React.Fragment>
<NotificationsPortal />
<Router />
<NotificationsProvider>
<Router />
</NotificationsProvider>
</React.Fragment>
);
};

View file

@ -5,7 +5,7 @@ import React from 'react';
import 'cockpit-dark-theme';
import { Page, PageSection } from '@patternfly/react-core';
import NotificationsPortal from '@redhat-cloud-services/frontend-components-notifications/NotificationPortal';
import NotificationsProvider from '@redhat-cloud-services/frontend-components-notifications/NotificationsProvider';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { HashRouter } from 'react-router-dom';
@ -31,10 +31,11 @@ const Application = () => {
return (
<React.Fragment>
<NotificationsPortal />
<HashRouter>
<Router />
</HashRouter>
<NotificationsProvider>
<HashRouter>
<Router />
</HashRouter>
</NotificationsProvider>
</React.Fragment>
);
};

View file

@ -10,15 +10,13 @@ import {
Spinner,
} from '@patternfly/react-core';
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
import {
selectSelectedBlueprintId,
setBlueprintId,
} from '../../store/BlueprintSlice';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import {
BlueprintItem,
useDeleteBlueprintMutation,
} from '../../store/imageBuilderApi';
import { BlueprintItem } from '../../store/imageBuilderApi';
type blueprintProps = {
blueprint: BlueprintItem;
@ -28,7 +26,7 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const dispatch = useAppDispatch();
const [, { isLoading }] = useDeleteBlueprintMutation({
const { isLoading } = useDeleteBlueprintMutation({
fixedCacheKey: 'delete-blueprint',
});

View file

@ -16,17 +16,14 @@ import {
} 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 { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { ChromeUser } from '@redhat-cloud-services/types';
import { skipToken } from '@reduxjs/toolkit/query';
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
import {
useGetBlueprintQuery,
useComposeBlueprintMutation,
} from '../../store/backendApi';
import { useComposeBPWithNotification as useComposeBlueprintMutation } from '../../Hooks';
import { useGetBlueprintQuery } from '../../store/backendApi';
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { useAppSelector } from '../../store/hooks';
import { ImageTypes } from '../../store/imageBuilderApi';
type BuildImagesButtonPropTypes = {
@ -37,9 +34,8 @@ type BuildImagesButtonPropTypes = {
export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const [deselectedTargets, setDeselectedTargets] = useState<ImageTypes[]>([]);
const [buildBlueprint, { isLoading: imageBuildLoading }] =
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
useComposeBlueprintMutation();
const dispatch = useAppDispatch();
const { analytics, auth } = useChrome();
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
@ -53,29 +49,19 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
const onBuildHandler = async () => {
if (selectedBlueprintId) {
try {
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',
});
} catch (imageBuildError) {
dispatch(
addNotification({
variant: 'warning',
title: 'No blueprint was build',
description: imageBuildError?.data?.error?.message,
})
);
}
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);
@ -180,7 +166,7 @@ export const BuildImagesButtonEmptyState = ({
children,
}: BuildImagesButtonEmptyStatePropTypes) => {
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
const [buildBlueprint, { isLoading: imageBuildLoading }] =
const { trigger: buildBlueprint, isLoading: imageBuildLoading } =
useComposeBlueprintMutation();
const onBuildHandler = async () => {
if (selectedBlueprintId) {

View file

@ -10,11 +10,8 @@ import {
PAGINATION_LIMIT,
PAGINATION_OFFSET,
} from '../../constants';
import {
backendApi,
useDeleteBlueprintMutation,
useGetBlueprintsQuery,
} from '../../store/backendApi';
import { useDeleteBPWithNotification as useDeleteBlueprintMutation } from '../../Hooks';
import { backendApi, useGetBlueprintsQuery } from '../../store/backendApi';
import {
selectBlueprintSearchInput,
selectLimit,
@ -65,7 +62,7 @@ export const DeleteBlueprintModal: React.FunctionComponent<
)?.name,
}),
});
const [deleteBlueprint] = useDeleteBlueprintMutation({
const { trigger: deleteBlueprint } = useDeleteBlueprintMutation({
fixedCacheKey: 'delete-blueprint',
});
const handleDelete = async () => {

View file

@ -16,7 +16,7 @@ import {
import { Modal, ModalVariant } from '@patternfly/react-core/deprecated';
import { DropEvent } from '@patternfly/react-core/dist/esm/helpers';
import { HelpIcon } from '@patternfly/react-icons';
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { useAddNotification } from '@redhat-cloud-services/frontend-components-notifications/hooks';
import { useNavigate } from 'react-router-dom';
import { mapOnPremToHosted } from './helpers/onPremToHostedBlueprintMapper';
@ -26,7 +26,6 @@ import {
ApiRepositoryRequest,
useBulkImportRepositoriesMutation,
} from '../../store/contentSourcesApi';
import { useAppDispatch } from '../../store/hooks';
import {
BlueprintExportResponse,
BlueprintItem,
@ -64,7 +63,7 @@ export const ImportBlueprintModal: React.FunctionComponent<
const [isRejected, setIsRejected] = React.useState(false);
const [isOnPrem, setIsOnPrem] = React.useState(false);
const [isCheckedImportRepos, setIsCheckedImportRepos] = React.useState(true);
const dispatch = useAppDispatch();
const addNotification = useAddNotification();
const [importRepositories] = useBulkImportRepositoriesMutation();
const handleFileInputChange = (
@ -103,33 +102,27 @@ export const ImportBlueprintModal: React.FunctionComponent<
importedRepositoryNames.push(repository.url);
return;
}
dispatch(
addNotification({
variant: 'warning',
title: 'Failed to import custom repositories',
description: JSON.stringify(repository.warnings),
})
);
addNotification({
variant: 'warning',
title: 'Failed to import custom repositories',
description: JSON.stringify(repository.warnings),
});
});
if (importedRepositoryNames.length !== 0) {
dispatch(
addNotification({
variant: 'info',
title: 'Successfully imported custom repositories',
description: importedRepositoryNames.join(', '),
})
);
addNotification({
variant: 'info',
title: 'Successfully imported custom repositories',
description: importedRepositoryNames.join(', '),
});
}
return newCustomRepos;
}
} catch {
dispatch(
addNotification({
variant: 'danger',
title: 'Custom repositories import failed',
})
);
addNotification({
variant: 'danger',
title: 'Custom repositories import failed',
});
}
}
}
@ -197,13 +190,11 @@ export const ImportBlueprintModal: React.FunctionComponent<
}
} catch (error) {
setIsInvalidFormat(true);
dispatch(
addNotification({
variant: 'warning',
title: 'File is not a valid blueprint',
description: error?.data?.error?.message,
})
);
addNotification({
variant: 'warning',
title: 'File is not a valid blueprint',
description: error?.data?.error?.message,
});
}
};
parseAndImport();

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { useAddNotification } from '@redhat-cloud-services/frontend-components-notifications/hooks';
import { useLocation, useNavigate } from 'react-router-dom';
import CreateImageWizard from './CreateImageWizard';
@ -13,6 +13,7 @@ const ImportImageWizard = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const location = useLocation();
const addNotification = useAddNotification();
const locationState = location.state as { blueprint?: wizardState };
const blueprint = locationState?.blueprint;
useEffect(() => {
@ -20,12 +21,10 @@ const ImportImageWizard = () => {
dispatch(loadWizardState(blueprint));
} else {
navigate(resolveRelPath(''));
dispatch(
addNotification({
variant: 'warning',
title: 'No blueprint was imported',
})
);
addNotification({
variant: 'warning',
title: 'No blueprint was imported',
});
}
}, [blueprint, dispatch]);
return <CreateImageWizard />;

View file

@ -13,7 +13,7 @@ import {
TextInputGroup,
TextInputGroupMain,
} from '@patternfly/react-core';
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { useAddNotification } from '@redhat-cloud-services/frontend-components-notifications/hooks';
import ManageKeysButton from './ManageKeysButton';
import PopoverActivation from './PopoverActivation';
@ -37,6 +37,7 @@ import { generateRandomId } from '../../../utilities/generateRandomId';
const ActivationKeysList = () => {
const dispatch = useAppDispatch();
const addNotification = useAddNotification();
const activationKey = useAppSelector(selectActivationKey);
const registrationType = useAppSelector(selectRegistrationType);
@ -138,13 +139,11 @@ const ActivationKeysList = () => {
);
dispatch(changeActivationKey(defaultActivationKeyName));
} catch (error) {
dispatch(
addNotification({
variant: 'danger',
title: 'Error creating activation key',
description: error?.data?.error?.message,
})
);
addNotification({
variant: 'danger',
title: 'Error creating activation key',
description: error?.data?.error?.message,
});
}
};

View file

@ -14,12 +14,15 @@ import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
import { useCreateBlueprintMutation } from '../../../../../store/backendApi';
import {
useComposeBPWithNotification as useComposeBlueprintMutation,
useCreateBPWithNotification as useCreateBlueprintMutation,
} from '../../../../../Hooks';
import { setBlueprintId } from '../../../../../store/BlueprintSlice';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import {
CreateBlueprintRequest,
useComposeBlueprintMutation,
CreateBlueprintResponse,
} from '../../../../../store/imageBuilderApi';
import { selectPackages } from '../../../../../store/wizardSlice';
import { createAnalytics } from '../../../../../Utilities/analytics';
@ -46,8 +49,8 @@ export const CreateSaveAndBuildBtn = ({
}, [auth]);
const packages = useAppSelector(selectPackages);
const [buildBlueprint] = useComposeBlueprintMutation();
const [createBlueprint] = useCreateBlueprintMutation({
const { trigger: buildBlueprint } = useComposeBlueprintMutation();
const { trigger: createBlueprint } = useCreateBlueprintMutation({
fixedCacheKey: 'createBlueprintKey',
});
const dispatch = useAppDispatch();
@ -70,13 +73,11 @@ export const CreateSaveAndBuildBtn = ({
),
});
}
const blueprint =
requestBody &&
(await createBlueprint({
if (requestBody) {
const blueprint = (await createBlueprint({
createBlueprintRequest: requestBody,
}).unwrap()); // unwrap - access the success payload immediately after a mutation
})) as CreateBlueprintResponse;
if (blueprint) {
buildBlueprint({ id: blueprint.id, body: {} });
dispatch(setBlueprintId(blueprint.id));
}
@ -107,7 +108,7 @@ export const CreateSaveButton = ({
}, [auth]);
const packages = useAppSelector(selectPackages);
const [createBlueprint, { isLoading }] = useCreateBlueprintMutation({
const { trigger: createBlueprint, isLoading } = useCreateBlueprintMutation({
fixedCacheKey: 'createBlueprintKey',
});
const dispatch = useAppDispatch();
@ -166,15 +167,11 @@ export const CreateSaveButton = ({
account_id: userData?.identity.internal?.account_id || 'Not found',
});
}
const blueprint =
requestBody &&
(await createBlueprint({
if (requestBody) {
const blueprint = (await createBlueprint({
createBlueprintRequest: requestBody,
}).unwrap());
if (blueprint) {
dispatch(setBlueprintId(blueprint?.id));
})) as CreateBlueprintResponse;
dispatch(setBlueprintId(blueprint.id));
}
};

View file

@ -12,12 +12,12 @@ import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
import { useUpdateBlueprintMutation } from '../../../../../store/backendApi';
import { useAppSelector } from '../../../../../store/hooks';
import {
CreateBlueprintRequest,
useComposeBlueprintMutation,
} from '../../../../../store/imageBuilderApi';
useComposeBPWithNotification as useComposeBlueprintMutation,
useUpdateBPWithNotification as useUpdateBlueprintMutation,
} from '../../../../../Hooks';
import { useAppSelector } from '../../../../../store/hooks';
import { CreateBlueprintRequest } from '../../../../../store/imageBuilderApi';
import { selectPackages } from '../../../../../store/wizardSlice';
import { createAnalytics } from '../../../../../Utilities/analytics';
@ -43,10 +43,10 @@ export const EditSaveAndBuildBtn = ({
setUserData(data);
})();
}, [auth]);
const [buildBlueprint] = useComposeBlueprintMutation();
const { trigger: buildBlueprint } = useComposeBlueprintMutation();
const packages = useAppSelector(selectPackages);
const [updateBlueprint] = useUpdateBlueprintMutation({
const { trigger: updateBlueprint } = useUpdateBlueprintMutation({
fixedCacheKey: 'updateBlueprintKey',
});
@ -104,7 +104,7 @@ export const EditSaveButton = ({
}, [auth]);
const packages = useAppSelector(selectPackages);
const [updateBlueprint, { isLoading }] = useUpdateBlueprintMutation({
const { trigger: updateBlueprint, isLoading } = useUpdateBlueprintMutation({
fixedCacheKey: 'updateBlueprintKey',
});
const onSave = async () => {

View file

@ -17,20 +17,20 @@ import { CreateSaveAndBuildBtn, CreateSaveButton } from './CreateDropdown';
import { EditSaveAndBuildBtn, EditSaveButton } from './EditDropdown';
import {
useCreateBlueprintMutation,
useUpdateBlueprintMutation,
} from '../../../../../store/backendApi';
useCreateBPWithNotification as useCreateBlueprintMutation,
useUpdateBPWithNotification as useUpdateBlueprintMutation,
} from '../../../../../Hooks';
import { resolveRelPath } from '../../../../../Utilities/path';
import { mapRequestFromState } from '../../../utilities/requestMapper';
import { useIsBlueprintValid } from '../../../utilities/useValidation';
const ReviewWizardFooter = () => {
const { goToPrevStep, close } = useWizardContext();
const [, { isSuccess: isCreateSuccess, reset: resetCreate }] =
const { isSuccess: isCreateSuccess, reset: resetCreate } =
useCreateBlueprintMutation({ fixedCacheKey: 'createBlueprintKey' });
// initialize the server store with the data from RTK query
const [, { isSuccess: isUpdateSuccess, reset: resetUpdate }] =
const { isSuccess: isUpdateSuccess, reset: resetUpdate } =
useUpdateBlueprintMutation({ fixedCacheKey: 'updateBlueprintKey' });
const { auth } = useChrome();
const { composeId } = useParams();

View file

@ -13,6 +13,7 @@ import {
Title,
} from '@patternfly/react-core';
import { useFixupBPWithNotification as useFixupBlueprintMutation } from '../../Hooks';
import {
useGetBlueprintsQuery,
useGetBlueprintQuery,
@ -28,7 +29,6 @@ import {
useGetBlueprintComposesQuery,
Distributions,
GetBlueprintComposesApiArg,
useFixupBlueprintMutation,
} from '../../store/imageBuilderApi';
import { BlueprintActionsMenu } from '../Blueprints/BlueprintActionsMenu';
import BlueprintDiffModal from '../Blueprints/BlueprintDiffModal';
@ -128,7 +128,7 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
{ skip: !selectedBlueprintId }
);
const [fixupBlueprint] = useFixupBlueprintMutation();
const { trigger: fixupBlueprint } = useFixupBlueprintMutation();
const hasErrors =
blueprintDetails?.lint?.errors && blueprintDetails?.lint?.errors.length > 0;
const [isLintExp, setIsLintExp] = React.useState(true);

View file

@ -24,7 +24,7 @@ import './LandingPage.scss';
import { NewAlert } from './NewAlert';
import { MANAGING_WITH_DNF_URL, OSTREE_URL } from '../../constants';
import { manageEdgeImagesUrlName } from '../../Utilities/edge';
import { manageEdgeImagesUrlName } from '../../Hooks/Edge/useGetNotificationProp';
import { resolveRelPath } from '../../Utilities/path';
import { useFlag } from '../../Utilities/useGetEnvironment';
import BlueprintsSidebar from '../Blueprints/BlueprintsSideBar';

View file

@ -29,9 +29,9 @@ import {
import { useNavigate } from 'react-router-dom';
import { AWS_REGIONS } from '../../constants';
import { useCloneComposeWithNotification as useCloneComposeMutation } from '../../Hooks';
import {
ComposeStatus,
useCloneComposeMutation,
useGetComposeStatusQuery,
} from '../../store/imageBuilderApi';
import { resolveRelPath } from '../../Utilities/path';
@ -130,7 +130,7 @@ const RegionsSelect = ({ composeId, handleClose }: RegionsSelectPropTypes) => {
}
};
const [cloneCompose] = useCloneComposeMutation();
const { trigger: cloneCompose } = useCloneComposeMutation();
const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({
composeId,

View file

@ -3,19 +3,17 @@ import React from 'react';
import AsyncComponent from '@redhat-cloud-services/frontend-components/AsyncComponent';
import ErrorState from '@redhat-cloud-services/frontend-components/ErrorState';
import Unavailable from '@redhat-cloud-services/frontend-components/Unavailable';
import { useDispatch } from 'react-redux';
import { useNavigate, useLocation, useParams } from 'react-router-dom';
import {
getNotificationProp,
useGetNotificationProp,
manageEdgeImagesUrlName,
} from '../../Utilities/edge';
} from '../../Hooks/Edge/useGetNotificationProp';
import { resolveRelPath } from '../../Utilities/path';
import { useFlag } from '../../Utilities/useGetEnvironment';
const ImageDetail = () => {
const dispatch = useDispatch();
const notificationProp = getNotificationProp(dispatch);
const notificationProp = useGetNotificationProp();
// Feature flag for the federated modules
const edgeParityFlag = useFlag('edgeParity.image-list');
// Feature flag to access the 'local' images table list

View file

@ -3,20 +3,18 @@ import React from 'react';
import AsyncComponent from '@redhat-cloud-services/frontend-components/AsyncComponent';
import ErrorState from '@redhat-cloud-services/frontend-components/ErrorState';
import Unavailable from '@redhat-cloud-services/frontend-components/Unavailable';
import { useDispatch } from 'react-redux';
import { useNavigate, useLocation } from 'react-router-dom';
import { CREATING_IMAGES_WITH_IB_URL } from '../../constants';
import {
getNotificationProp,
useGetNotificationProp,
manageEdgeImagesUrlName,
} from '../../Utilities/edge';
} from '../../Hooks/Edge/useGetNotificationProp';
import { resolveRelPath } from '../../Utilities/path';
import { useFlag } from '../../Utilities/useGetEnvironment';
const ImagesTable = () => {
const dispatch = useDispatch();
const notificationProp = getNotificationProp(dispatch);
const notificationProp = useGetNotificationProp();
// Feature flag for the federated modules
const edgeParityFlag = useFlag('edgeParity.image-list');
// Feature flag to access the 'local' images table list

View file

@ -0,0 +1,34 @@
import { useAddNotification } from '@redhat-cloud-services/frontend-components-notifications/hooks';
const manageEdgeImagesUrlName = 'manage-edge-images';
const useGetNotificationProp = () => {
const addNotification = useAddNotification();
return {
hasInfo: (hasInfoMessage: Notification) => {
addNotification({
variant: 'info',
...hasInfoMessage,
});
},
hasSuccess: (hasSuccessMessage: Notification) => {
addNotification({
variant: 'success',
...hasSuccessMessage,
});
},
/* eslint-disable @typescript-eslint/no-explicit-any */
err: (errMessage: any, err: any) => {
addNotification({
variant: 'danger',
...errMessage,
// Add error message from API, if present
description: err?.Title
? `${errMessage.description}: ${err.Title}`
: errMessage.description,
});
},
};
};
export { useGetNotificationProp, manageEdgeImagesUrlName };

View file

@ -0,0 +1,24 @@
import { useMutationWithNotification } from './useMutationWithNotification';
import {
CloneComposeApiArg,
useCloneComposeMutation,
} from '../../store/service/imageBuilderApi';
export const useCloneComposeWithNotification = () => {
const { trigger: cloneCompose, ...rest } = useMutationWithNotification(
useCloneComposeMutation,
{
messages: {
success: ({ cloneRequest }: CloneComposeApiArg) =>
`Your image is being shared to ${cloneRequest.region} region`,
error: () => 'Your image could not be shared',
},
}
);
return {
trigger: cloneCompose,
...rest,
};
};

View file

@ -0,0 +1,20 @@
import { useMutationWithNotification } from './useMutationWithNotification';
import { useComposeBlueprintMutation } from '../../store/backendApi';
export const useComposeBPWithNotification = () => {
const { trigger: composeBlueprint, ...rest } = useMutationWithNotification(
useComposeBlueprintMutation,
{
messages: {
success: () => 'Image is being built',
error: () => 'Image could not be built',
},
}
);
return {
trigger: composeBlueprint,
...rest,
};
};

View file

@ -0,0 +1,24 @@
import {
HookOptions,
useMutationWithNotification,
} from './useMutationWithNotification';
import { useCreateBlueprintMutation } from '../../store/backendApi';
export const useCreateBPWithNotification = (options?: HookOptions) => {
const { trigger: createBlueprint, ...rest } = useMutationWithNotification(
useCreateBlueprintMutation,
{
options,
messages: {
success: () => 'Blueprint was created',
error: () => 'Blueprint could not be created',
},
}
);
return {
trigger: createBlueprint,
...rest,
};
};

View file

@ -0,0 +1,24 @@
import {
HookOptions,
useMutationWithNotification,
} from './useMutationWithNotification';
import { useDeleteBlueprintMutation } from '../../store/backendApi';
export const useDeleteBPWithNotification = (options?: HookOptions) => {
const { trigger: deleteBlueprint, ...rest } = useMutationWithNotification(
useDeleteBlueprintMutation,
{
options,
messages: {
success: () => 'Blueprint was deleted',
error: () => 'Blueprint could not be deleted',
},
}
);
return {
trigger: deleteBlueprint,
...rest,
};
};

View file

@ -0,0 +1,24 @@
import {
HookOptions,
useMutationWithNotification,
} from './useMutationWithNotification';
import { useFixupBlueprintMutation } from '../../store/imageBuilderApi';
export const useFixupBPWithNotification = (options?: HookOptions) => {
const { trigger: fixupBlueprint, ...rest } = useMutationWithNotification(
useFixupBlueprintMutation,
{
options,
messages: {
success: () => 'Blueprint was fixed',
error: () => 'Blueprint could not be fixed',
},
}
);
return {
trigger: fixupBlueprint,
...rest,
};
};

View file

@ -0,0 +1,88 @@
import { useAddNotification } from '@redhat-cloud-services/frontend-components-notifications/hooks';
import {
BaseQueryFn,
TypedMutationTrigger,
} from '@reduxjs/toolkit/dist/query/react';
import { errorMessage } from '../../store/service/enhancedImageBuilderApi';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getErrorDescription = (err: any) => {
if (process.env.IS_ON_PREMISE) {
// If details are present, assume it's coming from composer
if (err.error?.body?.details) {
return `${err.error.message}: ${err.error.body.details}`;
}
return JSON.stringify(err);
}
if (err.error?.status) {
return `Status code ${err.error.status}: ${errorMessage(err)}`;
}
return err as string;
};
type NotificationMessages<TArgs> = {
success: (args: TArgs) => string;
error?: (args: TArgs, error: unknown) => string;
};
export type HookOptions = {
fixedCacheKey?: string | string;
};
type MutationOptions<Arg> = {
options?: HookOptions | undefined;
messages: NotificationMessages<Arg>;
};
// cursor ide was used to make this hook more generic
// and re-usable. Specifically for extending the complicated
// types to pass in other mutation hooks with using `any`
export function useMutationWithNotification<
Arg,
Result,
State extends {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
error?: unknown;
reset: () => void;
}
>(
mutationHook: (
options?: HookOptions
) => readonly [TypedMutationTrigger<Result, Arg, BaseQueryFn>, State],
{ options, messages }: MutationOptions<Arg>
) {
const [trigger, state] = mutationHook(options);
const addNotification = useAddNotification();
const handler = async (args: Arg): Promise<Result> => {
try {
const result = await trigger(args).unwrap();
addNotification({
variant: 'success',
title: messages.success(args),
});
return result;
} catch (err) {
const description = getErrorDescription(err);
if (messages.error) {
addNotification({
variant: 'danger',
title: messages.error(args, err),
description,
});
}
return err;
}
};
return {
trigger: handler,
...state,
};
}

View file

@ -0,0 +1,23 @@
import {
HookOptions,
useMutationWithNotification,
} from './useMutationWithNotification';
import { useUpdateBlueprintMutation } from '../../store/backendApi';
export const useUpdateBPWithNotification = (options?: HookOptions) => {
const { trigger: updateBlueprint, ...rest } = useMutationWithNotification(
useUpdateBlueprintMutation,
{
options,
messages: {
success: () => 'Blueprint was updated',
error: () => 'Blueprint could not be updated',
},
}
);
return {
trigger: updateBlueprint,
...rest,
};
};

6
src/Hooks/index.tsx Normal file
View file

@ -0,0 +1,6 @@
export { useCreateBPWithNotification } from './MutationNotifications/useCreateBPWithNotification';
export { useUpdateBPWithNotification } from './MutationNotifications/useUpdateBPWithNotification';
export { useDeleteBPWithNotification } from './MutationNotifications/useDeleteBPWithNotification';
export { useFixupBPWithNotification } from './MutationNotifications/useFixupBPWithNotification';
export { useComposeBPWithNotification } from './MutationNotifications/useComposeBPWithNotification';
export { useCloneComposeWithNotification } from './MutationNotifications/useCloneComposeWithNotification';

View file

@ -4,7 +4,7 @@ import { Route, Routes } from 'react-router-dom';
import EdgeImageDetail from './Components/edge/ImageDetails';
import ShareImageModal from './Components/ShareImageModal/ShareImageModal';
import { manageEdgeImagesUrlName } from './Utilities/edge';
import { manageEdgeImagesUrlName } from './Hooks/Edge/useGetNotificationProp';
import {
useFlag,
useFlagWithEphemDefault,

View file

@ -1,40 +0,0 @@
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { Dispatch } from 'redux';
const manageEdgeImagesUrlName = 'manage-edge-images';
const getNotificationProp = (dispatch: Dispatch) => {
return {
hasInfo: (hasInfoMessage: Notification) => {
dispatch({
...addNotification({
variant: 'info',
...hasInfoMessage,
}),
});
},
hasSuccess: (hasSuccessMessage: Notification) => {
dispatch({
...addNotification({
variant: 'success',
...hasSuccessMessage,
}),
});
},
/* eslint-disable @typescript-eslint/no-explicit-any */
err: (errMessage: any, err: any) => {
dispatch({
...addNotification({
variant: 'danger',
...errMessage,
// Add error message from API, if present
description: err?.Title
? `${errMessage.description}: ${err.Title}`
: errMessage.description,
}),
});
},
};
};
export { getNotificationProp, manageEdgeImagesUrlName };

View file

@ -1,5 +1,3 @@
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { cockpitApi } from './cockpitApi';
const enhancedApi = cockpitApi.enhanceEndpoints({
@ -17,98 +15,15 @@ const enhancedApi = cockpitApi.enhanceEndpoints({
},
createBlueprint: {
invalidatesTags: [{ type: 'Blueprints' }],
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
dispatch(
addNotification({
variant: 'success',
title: 'Blueprint was created',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Unable to create blueprint',
description: `Error: ${JSON.stringify(err)}`,
})
);
});
},
},
updateBlueprint: {
invalidatesTags: [{ type: 'Blueprint' }, { type: 'Blueprints' }],
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
dispatch(
addNotification({
variant: 'success',
title: 'Blueprint was created',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Unable to update blueprint',
description: `Error: ${JSON.stringify(err)}`,
})
);
});
},
},
deleteBlueprint: {
invalidatesTags: [{ type: 'Blueprints' }, { type: 'Composes' }],
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
dispatch(
addNotification({
variant: 'success',
title: 'Blueprint was deleted',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Blueprint could not be deleted',
description: `Error: ${JSON.stringify(err)}`,
})
);
});
},
},
composeBlueprint: {
invalidatesTags: [{ type: 'Composes' }],
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
dispatch(
addNotification({
variant: 'success',
title: 'Build was queued',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Unable to build blueprint',
// If details are present, assume it's coming from composer
description: err.error?.body?.details
? `${err.error.message}: ${err.error.body.details}`
: `Error: ${JSON.stringify(err)}`,
})
);
});
},
},
getComposes: {
providesTags: [{ type: 'Composes' }],

View file

@ -1,4 +1,3 @@
import { notificationsReducer } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import promiseMiddleware from 'redux-promise-middleware';
@ -27,7 +26,6 @@ export const serviceReducer = combineReducers({
[rhsmApi.reducerPath]: rhsmApi.reducer,
[provisioningApi.reducerPath]: provisioningApi.reducer,
[complianceApi.reducerPath]: complianceApi.reducer,
notifications: notificationsReducer,
wizard: wizardSlice,
blueprints: blueprintsSlice.reducer,
});
@ -41,7 +39,6 @@ export const onPremReducer = combineReducers({
// TODO: add other endpoints so we can remove this.
// It's still needed to get things to work.
[imageBuilderApi.reducerPath]: imageBuilderApi.reducer,
notifications: notificationsReducer,
wizard: wizardSlice,
blueprints: blueprintsSlice.reducer,
});
@ -64,7 +61,7 @@ startAppListening({
distribution: distribution,
})(state as serviceState);
const allowedImageTypes = architecturesResponse?.data?.find(
const allowedImageTypes = architecturesResponse.data?.find(
(elem) => elem.arch === architecture
)?.image_types;

View file

@ -1,5 +1,3 @@
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { imageBuilderApi } from '../imageBuilderApi';
/* eslint-disable @typescript-eslint/no-explicit-any */
@ -51,146 +49,45 @@ const enhancedApi = imageBuilderApi.enhanceEndpoints({
},
updateBlueprint: {
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
dispatch(
// @ts-expect-error Typescript is unaware of tag types being defined concurrently in enhanceEndpoints()
imageBuilderApi.util.invalidateTags(['Blueprints', 'Blueprint'])
);
dispatch(
addNotification({
variant: 'success',
title: 'Changes saved to blueprint',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Blueprint could not be updated',
description: `Status code ${err.error.status}: ${errorMessage(
err
)}`,
})
);
});
queryFulfilled.then(() => {
dispatch(
// @ts-expect-error Typescript is unaware of tag types being defined concurrently in enhanceEndpoints()
imageBuilderApi.util.invalidateTags(['Blueprints', 'Blueprint'])
);
});
},
},
createBlueprint: {
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
// @ts-expect-error Typescript is unaware of tag types being defined concurrently in enhanceEndpoints()
dispatch(imageBuilderApi.util.invalidateTags(['Blueprints']));
dispatch(
addNotification({
variant: 'success',
title: 'Blueprint is being created',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Blueprint could not be created',
description: `Status code ${err.error.status}: ${errorMessage(
err
)}`,
})
);
});
queryFulfilled.then(() => {
// @ts-expect-error Typescript is unaware of tag types being defined concurrently in enhanceEndpoints()
dispatch(imageBuilderApi.util.invalidateTags(['Blueprints']));
});
},
},
cloneCompose: {
onQueryStarted: async (
{ composeId, cloneRequest },
{ dispatch, queryFulfilled }
) => {
queryFulfilled
.then(() => {
dispatch(
imageBuilderApi.util.invalidateTags([
// @ts-expect-error Typescript is unaware of tag types being defined concurrently in enhanceEndpoints()
{ type: 'Clone', id: composeId },
])
);
dispatch(
addNotification({
variant: 'success',
title:
'Your image is being shared to ' +
cloneRequest.region +
' region',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Your image could not be shared',
description: `Status code ${err.error.status}: ${errorMessage(
err
)}`,
})
);
});
onQueryStarted: async ({ composeId }, { dispatch, queryFulfilled }) => {
queryFulfilled.then(() => {
dispatch(
imageBuilderApi.util.invalidateTags([
// @ts-expect-error Typescript is unaware of tag types being defined concurrently in enhanceEndpoints()
{ type: 'Clone', id: composeId },
])
);
});
},
},
composeBlueprint: {
invalidatesTags: [{ type: 'Compose' }, { type: 'BlueprintComposes' }],
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
dispatch(
addNotification({
variant: 'success',
title: 'Image is being built',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Image could not be built',
description: `Status code ${err.error.status}: ${errorMessage(
err
)}`,
})
);
});
},
},
composeImage: {
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
dispatch(
// @ts-expect-error Typescript is unaware of tag types being defined concurrently in enhanceEndpoints()
imageBuilderApi.util.invalidateTags(['Blueprints', 'Compose'])
);
dispatch(
addNotification({
variant: 'success',
title: 'Your image is being created',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Your image could not be created',
description: `Status code ${err.error.status}: ${errorMessage(
err
)}`,
})
);
});
queryFulfilled.then(() => {
dispatch(
// @ts-expect-error Typescript is unaware of tag types being defined concurrently in enhanceEndpoints()
imageBuilderApi.util.invalidateTags(['Blueprints', 'Compose'])
);
});
},
},
deleteBlueprint: {
@ -199,53 +96,9 @@ const enhancedApi = imageBuilderApi.enhanceEndpoints({
{ type: 'BlueprintComposes' },
{ type: 'Compose' },
],
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
dispatch(
addNotification({
variant: 'success',
title: 'Blueprint was deleted',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Blueprint could not be deleted',
description: `Status code ${err.error.status}: ${errorMessage(
err
)}`,
})
);
});
},
},
fixupBlueprint: {
invalidatesTags: [{ type: 'Blueprint' }],
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
queryFulfilled
.then(() => {
dispatch(
addNotification({
variant: 'success',
title: 'Blueprint was fixed',
})
);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Blueprint could not be fixed',
description: `Status code ${err.error.status}: ${errorMessage(
err
)}`,
})
);
});
},
},
},
});