From b59d9f5ec0ddc649c3869e5d301bb3757e7c972f Mon Sep 17 00:00:00 2001 From: Matthias Dellweg Date: Fri, 31 Jan 2025 21:15:45 +0100 Subject: [PATCH 1/2] Create selfcontained Alertlist component --- src/components/alerts.tsx | 94 +++++++++++++++++++++++++++++++++++++++ src/components/index.ts | 1 + src/layout.tsx | 2 + src/routes/root.tsx | 21 +++++---- 4 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 src/components/alerts.tsx diff --git a/src/components/alerts.tsx b/src/components/alerts.tsx new file mode 100644 index 0000000..f1dc2c3 --- /dev/null +++ b/src/components/alerts.tsx @@ -0,0 +1,94 @@ +import { AlertActionCloseButton, AlertVariant } from '@patternfly/react-core'; +import { type ReactNode, createContext, useContext, useState } from 'react'; +import { Alert } from 'src/components'; + +export interface AlertType { + key?: number; + id?: string; + variant: 'danger' | 'success' | 'warning' | 'info' | 'custom' | AlertVariant; + title: string | ReactNode; + description?: string | ReactNode; +} + +interface IAlertsContextType { + alerts: AlertType[]; + addAlert: (alert: AlertType) => void; + closeAlert: (id: number) => void; +} + +// Do not export this to keep alerts and closeAlert private. +const AlertsContext = createContext(undefined); + +// Provide an addAlert method. +// addAlert will add info alerts with 5s timeout. +// If an alert has a 'id' it will replace existing ones with the same 'id'. +export const useAddAlert = () => useContext(AlertsContext).addAlert; + +export const AlertsContextProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const [{ alerts }, setAlertState] = useState<{ + counter: number; + alerts: AlertType[]; + }>({ counter: 0, alerts: [] }); + + const addAlert = (alert: AlertType) => + setAlertState(({ counter, alerts }) => { + if (alert.variant === AlertVariant.info) { + setTimeout(() => closeAlert(counter), 5000); + } + return { + counter: counter + 1, + alerts: [ + ...alerts.filter((item) => alert.id === null || item.id != alert.id), + { ...alert, key: counter }, + ], + }; + }); + + const closeAlert = (key: number) => + setAlertState(({ counter, alerts }) => ({ + counter, + alerts: alerts.filter((item) => item.key !== key), + })); + + return ( + + {children} + + ); +}; + +export const AlertList = () => { + const { alerts, closeAlert } = useContext(AlertsContext); + + return ( +
+ {alerts.map(({ key, title, variant, description }) => ( + closeAlert(key)} /> + } + > + {description} + + ))} +
+ ); +}; diff --git a/src/components/index.ts b/src/components/index.ts index d9626b0..dd04c51 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,4 @@ +export { useAddAlert } from './alerts'; export { AccessTab } from './access-tab'; export { AlertList, type AlertType, closeAlert } from './alert-list'; export { AppliedFilters } from './applied-filters'; diff --git a/src/layout.tsx b/src/layout.tsx index 84f9728..2a71e65 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -27,6 +27,7 @@ import { SmallLogo, StatefulDropdown, } from 'src/components'; +import { AlertList } from './components/alerts'; import { PulpMenu } from './menu'; import { Paths, formatPath } from './paths'; import { useUserContext } from './user-context'; @@ -134,6 +135,7 @@ export const Layout = ({ children }: { children: ReactNode }) => { return ( + {children} {aboutModalVisible ? ( - - - {isNavigating && } - - - - - + + + + + {isNavigating && } + + + + + + ); } From bb15b880346d2272d40a79456a5356c187ecd593 Mon Sep 17 00:00:00 2001 From: Matthias Dellweg Date: Sat, 1 Feb 2025 09:27:33 +0100 Subject: [PATCH 2/2] Employ use of new alert hook --- ...ible-repository-collection-version-add.tsx | 17 +------ src/components/collection-header.tsx | 47 ++++--------------- src/components/container-repository-form.tsx | 40 ++++++---------- 3 files changed, 26 insertions(+), 78 deletions(-) diff --git a/src/actions/ansible-repository-collection-version-add.tsx b/src/actions/ansible-repository-collection-version-add.tsx index 780305f..66bbdee 100644 --- a/src/actions/ansible-repository-collection-version-add.tsx +++ b/src/actions/ansible-repository-collection-version-add.tsx @@ -8,12 +8,7 @@ import { CollectionVersionAPI, type CollectionVersionSearch, } from 'src/api'; -import { - AlertList, - type AlertType, - DetailList, - closeAlert, -} from 'src/components'; +import { DetailList, useAddAlert } from 'src/components'; import { handleHttpError, parsePulpIDFromURL, taskAlert } from 'src/utilities'; import { Action } from './action'; @@ -86,12 +81,9 @@ const AddCollectionVersionModal = ({ closeAction: () => void; sourceRepository: AnsibleRepositoryType; }) => { - const [alerts, setAlerts] = useState([]); const [selected, setSelected] = useState([]); - const addAlert = (alert: AlertType) => { - setAlerts([...alerts, alert]); - }; + const addAlert = useAddAlert(); // @ts-expect-error: TS2525: Initializer provides no value for this binding element and the binding element has no default value. const query = ({ params } = {}) => CollectionVersionAPI.list(params); @@ -209,11 +201,6 @@ const AddCollectionVersionModal = ({ title={t`Collection versions`} /> - - closeAlert(i, { alerts, setAlerts })} - /> ); }; diff --git a/src/components/collection-header.tsx b/src/components/collection-header.tsx index b971b4a..6a69c2b 100644 --- a/src/components/collection-header.tsx +++ b/src/components/collection-header.tsx @@ -29,8 +29,6 @@ import { import { useAppContext } from 'src/app-context'; import { Alert, - AlertList, - type AlertType, BaseHeader, type BreadcrumbType, Breadcrumbs, @@ -50,8 +48,9 @@ import { SignatureBadge, Spinner, UploadSignatureModal, - closeAlert, + useAddAlert, } from 'src/components'; +import { type AlertType } from 'src/components/alerts'; import { Paths, formatPath } from 'src/paths'; import { DeleteCollectionUtils, @@ -91,7 +90,6 @@ export const CollectionHeader = ({ reload, updateParams, }: IProps) => { - const [alerts, setAlerts] = useState([]); const [collectionVersion, setCollectionVersion] = useState(null); const [confirmDelete, setConfirmDelete] = useState(false); const [copyCollectionToRepositoryModal, setCopyCollectionToRepositoryModal] = @@ -117,11 +115,12 @@ export const CollectionHeader = ({ useState(false); const [versionToUploadCertificate, setVersionToUploadCertificate] = useState(undefined); + const addAlert = useAddAlert(); useEffect(() => { DeleteCollectionUtils.countUsedbyDependencies(collection) .then((count) => setDeletionBlocked(!!count)) - .catch((alert) => addAlert(alert)); + .catch(addAlert); NamespaceAPI.get(collection.collection_version.namespace, { include_related: 'my_permissions', @@ -139,7 +138,6 @@ export const CollectionHeader = ({ const context = useAppContext(); const { featureFlags: { can_upload_signatures, display_signatures }, - queueAlert, settings: { GALAXY_COLLECTION_SIGNING_SERVICE }, } = context; @@ -313,7 +311,7 @@ export const CollectionHeader = ({ redirect: formatPath(Paths.ansible.namespace.detail, { namespace: deleteCollection.collection_version.namespace, }), - addAlert: (alert) => queueAlert(alert), + addAlert, deleteFromRepo, }); } @@ -322,7 +320,7 @@ export const CollectionHeader = ({ /> {copyCollectionToRepositoryModal && ( addAlert(alert)} + addAlert={addAlert} closeAction={() => setCopyCollectionToRepositoryModal(null)} collectionVersion={collection} /> @@ -464,15 +462,6 @@ export const CollectionHeader = ({ title={t`This collection has been deprecated.`} /> )} - - closeAlert(i, { - alerts, - setAlerts, - }) - } - />
{renderTabs(activeTab)}
@@ -604,20 +593,16 @@ export const CollectionHeader = ({ if (reload) { reload(); } - setAlerts((alerts) => - alerts.filter(({ id }) => id !== 'upload-certificate'), - ); addAlert({ + id: 'upload-certificate', variant: 'success', title: t`Certificate for collection "${version.namespace} ${version.name} v${version.version}" has been successfully uploaded.`, }); }); }) .catch((error) => { - setAlerts((alerts) => - alerts.filter(({ id }) => id !== 'upload-certificate'), - ); addAlert({ + id: 'upload-certificate', variant: 'danger', title: t`The certificate for "${version.namespace} ${version.name} v${version.version}" could not be saved.`, description: error, @@ -685,11 +670,6 @@ export const CollectionHeader = ({ waitForTask(result.data.task_id) .then(() => updateParams({})) .catch((error) => addAlert(errorAlert(error))) - .finally(() => - setAlerts((alerts) => - alerts.filter(({ id }) => id !== 'loading-signing'), - ), - ); }) .catch((error) => // The request failed in the first place @@ -725,11 +705,6 @@ export const CollectionHeader = ({ waitForTask(result.data.task_id) .then(() => updateParams({})) .catch((error) => addAlert(errorAlert(error))) - .finally(() => - setAlerts((alerts) => - alerts.filter(({ id }) => id !== 'loading-signing'), - ), - ); }) .catch((error) => // The request failed in the first place @@ -815,7 +790,7 @@ export const CollectionHeader = ({ }); } else { // last version in collection => collection will be deleted => redirect - queueAlert({ + addAlert({ variant: 'success', title: t`Collection "${name} v${collectionVersion}" has been successfully deleted.`, }); @@ -890,8 +865,4 @@ export const CollectionHeader = ({ function copyToRepository(collection: CollectionVersionSearch) { setCopyCollectionToRepositoryModal(collection); } - - function addAlert(alert: AlertType) { - setAlerts((alerts) => [...alerts, alert]); - } }; diff --git a/src/components/container-repository-form.tsx b/src/components/container-repository-form.tsx index 5ba0506..09b817f 100644 --- a/src/components/container-repository-form.tsx +++ b/src/components/container-repository-form.tsx @@ -18,15 +18,14 @@ import { ExecutionEnvironmentRemoteAPI, } from 'src/api'; import { - AlertList, - type AlertType, FormFieldHelper, HelpButton, LabelGroup, Spinner, Typeahead, - closeAlert, + useAddAlert, } from 'src/components'; +import { type AlertType } from 'src/components/alerts'; import { type ErrorMessagesType, alertErrorsWithoutFields, @@ -53,13 +52,15 @@ interface IProps { registry?: string; // pk upstreamName?: string; remoteId?: string; - addAlert?: (variant, title, description?) => void; +} + +interface IPropsImpl extends IProps { + addAlert: (alert: AlertType) => void; } interface IState { name: string; description: string; - alerts: AlertType[]; addTagsInclude: string; addTagsExclude: string; excludeTags?: string[]; @@ -70,7 +71,13 @@ interface IState { formErrors: ErrorMessagesType; } -export class ContainerRepositoryForm extends Component { +export const ContainerRepositoryForm = (props: IProps) => { + const addAlert = useAddAlert(); + + return ; +}; + +class ContainerRepositoryFormImpl extends Component { constructor(props) { super(props); this.state = { @@ -85,7 +92,6 @@ export class ContainerRepositoryForm extends Component { registrySelection: [], upstreamName: this.props.upstreamName || '', formErrors: {}, - alerts: [], }; } @@ -105,7 +111,7 @@ export class ContainerRepositoryForm extends Component { .catch((e) => { const { status, statusText } = e.response; const errorTitle = t`Registries list could not be displayed.`; - this.addAlert({ + this.props.addAlert({ variant: 'danger', title: errorTitle, description: jsxErrorMessage(status, statusText), @@ -122,7 +128,6 @@ export class ContainerRepositoryForm extends Component { const { addTagsExclude, addTagsInclude, - alerts, description, excludeTags, formErrors, @@ -153,15 +158,6 @@ export class ContainerRepositoryForm extends Component { , ]} > - - closeAlert(i, { - alerts, - setAlerts: (alerts) => this.setState({ alerts }), - }) - } - />
{!isRemote ? ( <> @@ -518,17 +514,11 @@ export class ContainerRepositoryForm extends Component { alertErrorsWithoutFields( this.state.formErrors, ['name', 'registry', 'registries'], - (alert) => this.addAlert(alert), + this.props.addAlert, t`Error when saving registry.`, (state) => this.setState({ formErrors: state }), ); return Promise.reject(new Error(e)); }); } - - private addAlert(alert) { - this.setState({ - alerts: [...this.state.alerts, alert], - }); - } }