From 6280108589b77c18697554baf40dd5a11324ea03 Mon Sep 17 00:00:00 2001 From: "bugs-buddy-jira-ai-issue-solver[bot]" <252868304+bugs-buddy-jira-ai-issue-solver[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:09:59 +0000 Subject: [PATCH 1/4] EDM-4157: [SPUR] Add divider before Delete Co-authored-by: Celia Amador Gonzalez --- .../AuthProvider/AuthProviderDetails/AuthProviderDetails.tsx | 3 ++- .../src/components/AuthProvider/AuthProviderRow.tsx | 1 + libs/ui-components/src/components/Catalog/CatalogPage.tsx | 1 + .../src/components/Device/DeviceDetails/DeviceDetailsPage.tsx | 3 ++- .../Device/DevicesPage/DecommissionedDeviceTableRow.tsx | 3 ++- .../components/Device/DevicesPage/EnrolledDeviceTableRow.tsx | 1 + .../EnrollmentRequestDetails/EnrollmentRequestDetails.tsx | 2 ++ .../components/EnrollmentRequest/EnrollmentRequestTableRow.tsx | 3 +++ .../src/components/Fleet/FleetDetails/FleetDetailsPage.tsx | 3 ++- libs/ui-components/src/components/Fleet/FleetRow.tsx | 1 + .../ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx | 3 ++- .../ui-components/src/components/ImageBuilds/ImageBuildRow.tsx | 2 ++ .../Repository/RepositoryDetails/RepositoryDetails.tsx | 3 ++- .../ui-components/src/components/Repository/RepositoryList.tsx | 3 +++ 14 files changed, 26 insertions(+), 6 deletions(-) diff --git a/libs/ui-components/src/components/AuthProvider/AuthProviderDetails/AuthProviderDetails.tsx b/libs/ui-components/src/components/AuthProvider/AuthProviderDetails/AuthProviderDetails.tsx index 49db9ab8c..690c97e51 100644 --- a/libs/ui-components/src/components/AuthProvider/AuthProviderDetails/AuthProviderDetails.tsx +++ b/libs/ui-components/src/components/AuthProvider/AuthProviderDetails/AuthProviderDetails.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { DropdownItem, DropdownList, Tab } from '@patternfly/react-core'; +import { Divider, DropdownItem, DropdownList, Tab } from '@patternfly/react-core'; import { AuthProvider } from '@flightctl/types'; @@ -70,6 +70,7 @@ const AuthProviderDetails = () => { {t('Edit authentication provider')} )} + {canDelete && canEdit && } {canDelete && ( setIsDeleteModalOpen(true)}> {t('Delete authentication provider')} diff --git a/libs/ui-components/src/components/AuthProvider/AuthProviderRow.tsx b/libs/ui-components/src/components/AuthProvider/AuthProviderRow.tsx index cb8122c1c..aaabf9020 100644 --- a/libs/ui-components/src/components/AuthProvider/AuthProviderRow.tsx +++ b/libs/ui-components/src/components/AuthProvider/AuthProviderRow.tsx @@ -48,6 +48,7 @@ const AuthProviderRow = ({ provider, onDeleteClick }: { provider: AuthProvider; } if (canDelete) { + actions.push({ isSeparator: true } as IAction); actions.push({ title: t('Delete'), onClick: onDeleteClick, diff --git a/libs/ui-components/src/components/Catalog/CatalogPage.tsx b/libs/ui-components/src/components/Catalog/CatalogPage.tsx index 4125099d3..18cbaa9cf 100644 --- a/libs/ui-components/src/components/Catalog/CatalogPage.tsx +++ b/libs/ui-components/src/components/Catalog/CatalogPage.tsx @@ -287,6 +287,7 @@ export const CatalogPageContent = ({ setCatalogToEdit(c)}> {!!c.metadata.owner || !canEditCatalog ? t('View') : t('Edit')} + )} {canResume && resumeAction} + {canDecommission && (hasEditPermissions || canResume) && } {canDecommission && decommissionAction} diff --git a/libs/ui-components/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx b/libs/ui-components/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx index 5b41047f7..f6d7f2a5e 100644 --- a/libs/ui-components/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx +++ b/libs/ui-components/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { ActionsColumn, OnSelect, Td, Tr } from '@patternfly/react-table'; +import { ActionsColumn, IAction, OnSelect, Td, Tr } from '@patternfly/react-table'; import { Device } from '@flightctl/types'; import { ListAction } from '../../ListPage/types'; @@ -74,6 +74,7 @@ const DecommissionedDeviceTableRow = ({ }, ...(canDelete ? [ + { isSeparator: true } as IAction, deleteAction({ resourceId: deviceName, resourceName: deviceAlias, diff --git a/libs/ui-components/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx b/libs/ui-components/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx index 859b1d932..04c2ddc24 100644 --- a/libs/ui-components/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx +++ b/libs/ui-components/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx @@ -80,6 +80,7 @@ const EnrolledDeviceTableRow = ({ : []), ...(canDecommission && decommissionAction ? [ + { isSeparator: true } as IAction, decommissionAction({ resourceId: deviceName, resourceName: deviceAlias, diff --git a/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestDetails/EnrollmentRequestDetails.tsx b/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestDetails/EnrollmentRequestDetails.tsx index 02f467ae6..47f0dc597 100644 --- a/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestDetails/EnrollmentRequestDetails.tsx +++ b/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestDetails/EnrollmentRequestDetails.tsx @@ -7,6 +7,7 @@ import { DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, + Divider, DropdownItem, DropdownList, Grid, @@ -92,6 +93,7 @@ const EnrollmentRequestDetails = () => { {t('Approve')} )} + {canDelete && canApprove && } {canDelete && deleteAction} diff --git a/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestTableRow.tsx b/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestTableRow.tsx index 0a671b34f..3cca3538e 100644 --- a/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestTableRow.tsx +++ b/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestTableRow.tsx @@ -46,6 +46,9 @@ const EnrollmentRequestTableRow: React.FC = ({ }); } if (canDelete) { + if (actionItems.length > 0) { + actionItems.push({ isSeparator: true } as IAction); + } actionItems.push(deleteAction({ resourceId: erName })); } diff --git a/libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx b/libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx index 03a36d03e..591ab3e4f 100644 --- a/libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx +++ b/libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { DropdownItem, DropdownList, Tab } from '@patternfly/react-core'; +import { Divider, DropdownItem, DropdownList, Tab } from '@patternfly/react-core'; import { Fleet } from '@flightctl/types'; import { RESOURCE, VERB } from '../../../types/rbac'; @@ -81,6 +81,7 @@ const FleetDetailPage = () => { )} + {canDelete && (isManaged || canEdit) && } {canDelete && ( = ({ const fleetRolloutError = getFleetRolloutStatusWarning(fleet, t); if (canDelete) { + actions.push({ isSeparator: true } as IAction); actions.push({ title: t('Delete fleet'), 'data-testid': 'fleet-row-menu-delete-fleet', diff --git a/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx index 98d543c51..226515594 100644 --- a/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx +++ b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { DropdownItem, DropdownList, Tab } from '@patternfly/react-core'; +import { Divider, DropdownItem, DropdownList, Tab } from '@patternfly/react-core'; import { RESOURCE, VERB } from '../../../types/rbac'; import PageWithPermissions from '../../common/PageWithPermissions'; @@ -83,6 +83,7 @@ const ImageBuildDetailsPageContent = () => { {canPromote && ( setIsImagePromotionOpen(true)}>{t('Add to catalog')} )} + {(canCancel || (canDelete && !canCancel)) && (canNewVersion || canPromote) && } {canCancel && ( setIsCancelModalOpen(true)}>{t('Cancel image build')} )} diff --git a/libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx b/libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx index 95946dfae..b16a00126 100644 --- a/libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx +++ b/libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx @@ -78,11 +78,13 @@ const ImageBuildRow = ({ } if (canCancel && isImageBuildCancelable(buildReason)) { + actions.push({ isSeparator: true } as IAction); actions.push({ title: t('Cancel image build'), onClick: onCancelClick, }); } else if (canDelete) { + actions.push({ isSeparator: true } as IAction); actions.push({ title: t('Delete image build'), onClick: onDeleteClick, diff --git a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx index 3054de092..7a7a76058 100644 --- a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { DropdownItem, DropdownList, Grid, GridItem, Tab } from '@patternfly/react-core'; +import { Divider, DropdownItem, DropdownList, Grid, GridItem, Tab } from '@patternfly/react-core'; import { useFetchPeriodically } from '../../../hooks/useFetchPeriodically'; import { RepoSpecType, Repository, ResourceKind } from '@flightctl/types'; @@ -62,6 +62,7 @@ const RepositoryDetails = () => { {t('Edit repository')} )} + {canDelete && canEdit && } {canDelete && ( setIsDeleteModalOpen(true)}>{t('Delete repository')} )} diff --git a/libs/ui-components/src/components/Repository/RepositoryList.tsx b/libs/ui-components/src/components/Repository/RepositoryList.tsx index 324a40f02..dc7a35639 100644 --- a/libs/ui-components/src/components/Repository/RepositoryList.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryList.tsx @@ -115,6 +115,9 @@ const RepositoryTableRow = ({ } as IAction); } if (canDelete) { + if (actions.length > 0) { + actions.push({ isSeparator: true } as IAction); + } actions.push({ title: t('Delete repository'), onClick: () => setDeleteModalRepoId(repository.metadata.name), From 64c144e4808408edaaf625c3b99ad5c3a94cca3f Mon Sep 17 00:00:00 2001 From: Celia Amador Date: Wed, 17 Jun 2026 12:31:36 +0200 Subject: [PATCH 2/4] EDM-4157: Define common ways to separate danger actions Made-with: Cursor --- libs/i18n/locales/en/translation.json | 3 +- .../AuthProviderDetails.tsx | 40 ++++--- .../AuthProvider/AuthProviderRow.tsx | 21 ++-- .../components/Catalog/CatalogItemDetails.tsx | 108 +++++++++--------- .../src/components/Catalog/CatalogPage.tsx | 41 ++++--- .../components/Catalog/InstalledSoftware.tsx | 50 ++++---- .../DeviceDetails/DeviceDetailsPage.tsx | 28 ++--- .../DecommissionedDeviceTableRow.tsx | 59 +++++----- .../DevicesPage/EnrolledDeviceTableRow.tsx | 12 +- .../EnrollmentRequestDetails.tsx | 18 +-- .../EnrollmentRequestTableRow.tsx | 13 +-- .../Fleet/FleetDetails/FleetDetailsPage.tsx | 75 ++++++------ .../src/components/Fleet/FleetRow.tsx | 38 +++--- .../ImageBuildDetailsPage.tsx | 30 +++-- .../ImageBuildDetailsTab.tsx | 36 +++--- .../components/ImageBuilds/ImageBuildRow.tsx | 15 +-- .../RepositoryDetails/RepositoryDetails.tsx | 20 ++-- .../components/Repository/RepositoryList.tsx | 25 ++-- .../components/common/ActionsDropdownList.tsx | 56 +++++++++ 19 files changed, 394 insertions(+), 294 deletions(-) create mode 100644 libs/ui-components/src/components/common/ActionsDropdownList.tsx diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 7c3db06d0..b11105c2e 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -344,7 +344,7 @@ "Data": "Data", "Actions dropdown": "Actions dropdown", "This catalog is managed by a resource sync and cannot be directly removed. Either remove the catalog definition from the resource sync configuration, or delete the resource sync first.": "This catalog is managed by a resource sync and cannot be directly removed. Either remove the catalog definition from the resource sync configuration, or delete the resource sync first.", - "Remove": "Remove", + "Delete catalog": "Delete catalog", "Category": "Category", "Get started": "Get started", "Help cards": "Help cards", @@ -360,7 +360,6 @@ "Are you sure you want to delete the catalog <1>{catalogDisplayName}?": "Are you sure you want to delete the catalog <1>{catalogDisplayName}?", "Checking if the catalog has items": "Checking if the catalog has items", "Reload catalog items": "Reload catalog items", - "Delete catalog": "Delete catalog", "Deprecate catalog item": "Deprecate catalog item", "Are you sure you want to deprecate <1>{itemName}?": "Are you sure you want to deprecate <1>{itemName}?", "Reason": "Reason", diff --git a/libs/ui-components/src/components/AuthProvider/AuthProviderDetails/AuthProviderDetails.tsx b/libs/ui-components/src/components/AuthProvider/AuthProviderDetails/AuthProviderDetails.tsx index 690c97e51..72835231e 100644 --- a/libs/ui-components/src/components/AuthProvider/AuthProviderDetails/AuthProviderDetails.tsx +++ b/libs/ui-components/src/components/AuthProvider/AuthProviderDetails/AuthProviderDetails.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Divider, DropdownItem, DropdownList, Tab } from '@patternfly/react-core'; +import { DropdownItem, Tab } from '@patternfly/react-core'; import { AuthProvider } from '@flightctl/types'; @@ -17,6 +17,7 @@ import { usePermissionsContext } from '../../common/PermissionsContext'; import YamlEditor from '../../common/CodeEditor/YamlEditor'; import { ProviderType } from '../../../types/extraTypes'; import TabsNav from '../../TabsNav/TabsNav'; +import ActionsDropdownList from '../../common/ActionsDropdownList'; const authProviderDetailsPermissions = [ { kind: RESOURCE.AUTH_PROVIDER, verb: VERB.DELETE }, @@ -56,27 +57,30 @@ const AuthProviderDetails = () => { actions={ (canDelete || canEdit) && ( - + {canEdit && ( - navigate({ route: ROUTE.AUTH_PROVIDER_EDIT, postfix: authProviderId })} - isAriaDisabled={isOAuth2} - tooltipProps={ - isOAuth2 - ? { content: {t('OAuth2 providers can only be edited via the YAML editor')} } - : undefined - } - > - {t('Edit authentication provider')} - + + navigate({ route: ROUTE.AUTH_PROVIDER_EDIT, postfix: authProviderId })} + isAriaDisabled={isOAuth2} + tooltipProps={ + isOAuth2 + ? { content: {t('OAuth2 providers can only be edited via the YAML editor')} } + : undefined + } + > + {t('Edit authentication provider')} + + )} - {canDelete && canEdit && } {canDelete && ( - setIsDeleteModalOpen(true)}> - {t('Delete authentication provider')} - + + setIsDeleteModalOpen(true)}> + {t('Delete authentication provider')} + + )} - + ) } diff --git a/libs/ui-components/src/components/AuthProvider/AuthProviderRow.tsx b/libs/ui-components/src/components/AuthProvider/AuthProviderRow.tsx index aaabf9020..8e85afbb3 100644 --- a/libs/ui-components/src/components/AuthProvider/AuthProviderRow.tsx +++ b/libs/ui-components/src/components/AuthProvider/AuthProviderRow.tsx @@ -11,6 +11,7 @@ import { DynamicAuthProviderSpec, ProviderType } from '../../types/extraTypes'; import { isOAuth2Provider } from './CreateAuthProvider/types'; import { getProviderTypeLabel } from './CreateAuthProvider/utils'; import { usePermissionsContext } from '../common/PermissionsContext'; +import { buildAllDropdownActions } from '../common/ActionsDropdownList'; const authProviderPermissions = [ { kind: RESOURCE.AUTH_PROVIDER, verb: VERB.UPDATE }, @@ -26,7 +27,7 @@ const AuthProviderRow = ({ provider, onDeleteClick }: { provider: AuthProvider; const { checkPermissions } = usePermissionsContext(); const [canEdit, canDelete] = checkPermissions(authProviderPermissions); - const actions: IAction[] = [ + const regularActions: IAction[] = [ { title: t('View details'), onClick: () => navigate({ route: ROUTE.AUTH_PROVIDER_DETAILS, postfix: providerName }), @@ -35,7 +36,7 @@ const AuthProviderRow = ({ provider, onDeleteClick }: { provider: AuthProvider; if (canEdit) { const isDisableEdit = providerSpec.providerType === ProviderType.OAuth2; - actions.push({ + regularActions.push({ title: t('Edit'), isAriaDisabled: isDisableEdit, tooltipProps: isDisableEdit @@ -47,13 +48,15 @@ const AuthProviderRow = ({ provider, onDeleteClick }: { provider: AuthProvider; }); } - if (canDelete) { - actions.push({ isSeparator: true } as IAction); - actions.push({ - title: t('Delete'), - onClick: onDeleteClick, - }); - } + const dangerActions: IAction[] = canDelete + ? [ + { + title: t('Delete'), + onClick: onDeleteClick, + }, + ] + : []; + const actions = buildAllDropdownActions(regularActions, dangerActions); let url: string = 'N/A'; let urlTitle: string = ''; diff --git a/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx b/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx index 92bee499d..cb4db096d 100644 --- a/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx +++ b/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx @@ -26,7 +26,7 @@ import * as React from 'react'; import * as semver from 'semver'; import ReactMarkdown from 'react-markdown'; import { Formik, useFormikContext } from 'formik'; -import { ActionsColumn } from '@patternfly/react-table'; +import { ActionsColumn, IAction } from '@patternfly/react-table'; import { useTranslation } from '../../hooks/useTranslation'; import { useFetch } from '../../hooks/useFetch'; @@ -38,6 +38,7 @@ import { getCatalogItemIcon, getFullContainerURI } from './utils'; import DeleteModal from '../modals/DeleteModal/DeleteModal'; import { useFetchPeriodically } from '../../hooks/useFetchPeriodically'; import WithTooltip from '../common/WithTooltip'; +import { buildAllDropdownActions } from '../common/ActionsDropdownList'; import FlightCtlPageDrawer from '../common/FlightCtlPageDrawer'; import { InstallSpecFormik } from './InstallWizard/types'; @@ -129,63 +130,64 @@ const CatalogItemDetailsPanel = ({ const isManaged = !!item.metadata.owner; + const regularActions: IAction[] = [ + { + title: isManaged ? t('View') : t('Edit'), + onClick: () => { + navigate({ + route: ROUTE.CATALOG_EDIT_ITEM, + postfix: `${item.metadata.catalog}/${item.metadata.name}`, + }); + }, + }, + isDeprecated + ? { + title: t('Restore'), + onClick: () => setIsRestoreModalOpen(true), + tooltipProps: isManaged + ? { + content: t( + "This catalog item is managed by a resource sync and cannot be directly restored. Either remove this catalog's definition from the resource sync configuration, or delete the resource sync first.", + ), + } + : undefined, + isAriaDisabled: isManaged, + } + : { + title: t('Deprecate'), + onClick: () => setIsDeprecateModalOpen(true), + tooltipProps: isManaged + ? { + content: t( + "This catalog item is managed by a resource sync and cannot be directly deprecated. Either remove this catalog's definition from the resource sync configuration, or delete the resource sync first.", + ), + } + : undefined, + isAriaDisabled: isManaged, + }, + ]; + const dangerActions: IAction[] = [ + { + title: t('Delete'), + onClick: () => setIsDeleteModalOpen(true), + tooltipProps: isManaged + ? { + content: t( + "This catalog item is managed by a resource sync and cannot be directly deleted. Either remove this catalog's definition from the resource sync configuration, or delete the resource sync first.", + ), + } + : undefined, + isAriaDisabled: isManaged, + }, + ]; + const catalogItemActions = buildAllDropdownActions(regularActions, dangerActions); + const panelContent = ( <> - {showCatalogMgmt && ( - { - navigate({ - route: ROUTE.CATALOG_EDIT_ITEM, - postfix: `${item.metadata.catalog}/${item.metadata.name}`, - }); - }, - }, - isDeprecated - ? { - title: t('Restore'), - onClick: () => setIsRestoreModalOpen(true), - tooltipProps: isManaged - ? { - content: t( - "This catalog item is managed by a resource sync and cannot be directly restored. Either remove this catalog's definition from the resource sync configuration, or delete the resource sync first.", - ), - } - : undefined, - isAriaDisabled: isManaged, - } - : { - title: t('Deprecate'), - onClick: () => setIsDeprecateModalOpen(true), - tooltipProps: isManaged - ? { - content: t( - "This catalog item is managed by a resource sync and cannot be directly deprecated. Either remove this catalog's definition from the resource sync configuration, or delete the resource sync first.", - ), - } - : undefined, - isAriaDisabled: isManaged, - }, - { - title: t('Delete'), - onClick: () => setIsDeleteModalOpen(true), - tooltipProps: isManaged - ? { - content: t( - "This catalog item is managed by a resource sync and cannot be directly deleted. Either remove this catalog's definition from the resource sync configuration, or delete the resource sync first.", - ), - } - : undefined, - isAriaDisabled: isManaged, - }, - ]} - /> - )} + {showCatalogMgmt && } diff --git a/libs/ui-components/src/components/Catalog/CatalogPage.tsx b/libs/ui-components/src/components/Catalog/CatalogPage.tsx index 18cbaa9cf..5207a756e 100644 --- a/libs/ui-components/src/components/Catalog/CatalogPage.tsx +++ b/libs/ui-components/src/components/Catalog/CatalogPage.tsx @@ -6,7 +6,6 @@ import { Divider, Dropdown, DropdownItem, - DropdownList, EmptyStateActions, EmptyStateBody, EmptyStateFooter, @@ -41,6 +40,7 @@ import { useFetchPeriodically } from '../../hooks/useFetchPeriodically'; import DeleteCatalogModal from './DeleteCatalogModal'; import CreateCatalogModal from './AddCatalogItemWizard/CreateCatalogModal'; import WithTooltip from '../common/WithTooltip'; +import ActionsDropdownList from '../common/ActionsDropdownList'; import ResourceSyncImportStatus from '../ResourceSync/ResourceSyncImportStatus'; import CatalogLandingPage, { CatalogLandingPageContent, useLandingPagePermissions } from './CatalogLandingPage'; import PageWithPermissions from '../common/PageWithPermissions'; @@ -283,25 +283,28 @@ export const CatalogPageContent = ({ /> )} > - - setCatalogToEdit(c)}> - {!!c.metadata.owner || !canEditCatalog ? t('View') : t('Edit')} - - - - setCatalogToDelete(c) : undefined} - > - {t('Remove')} + + + setCatalogToEdit(c)}> + {!!c.metadata.owner || !canEditCatalog ? t('View catalog') : t('Edit catalog')} - - + + + + setCatalogToDelete(c) : undefined} + > + {t('Delete catalog')} + + + + ) : undefined, }; diff --git a/libs/ui-components/src/components/Catalog/InstalledSoftware.tsx b/libs/ui-components/src/components/Catalog/InstalledSoftware.tsx index b08a41b4d..4208685e0 100644 --- a/libs/ui-components/src/components/Catalog/InstalledSoftware.tsx +++ b/libs/ui-components/src/components/Catalog/InstalledSoftware.tsx @@ -28,6 +28,7 @@ import { getCatalogItemIcon, getFullContainerURI, getUpdates } from './utils'; import { useFetch } from '../../hooks/useFetch'; import { useTranslation } from '../../hooks/useTranslation'; import DeleteModal from '../modals/DeleteModal/DeleteModal'; +import { buildAllDropdownActions } from '../common/ActionsDropdownList'; import { APP_CATALOG_LABEL_KEY, APP_CHANNEL_LABEL_KEY, @@ -196,6 +197,23 @@ const InstalledSoftware = ({ labels, spec, onDeleteOs, onDeleteApp, onEdit, canE const hasApps = !!(appItems && appItems.length > 0); const isEmpty = !hasOs && !hasApps; + const osActions = osItem + ? buildAllDropdownActions( + [ + { + title: t('Edit'), + onClick: () => onEdit(osItem.metadata.catalog, osItem.metadata.name || ''), + }, + ], + [ + { + title: t('Delete'), + onClick: () => setDeleteOs(true), + }, + ], + ) + : []; + return ( <> @@ -237,18 +255,7 @@ const InstalledSoftware = ({ labels, spec, onDeleteOs, onDeleteApp, onEdit, canE )} {canEdit && ( - onEdit(osItem.metadata.catalog, osItem.metadata.name || ''), - }, - { - title: t('Delete'), - onClick: () => setDeleteOs(true), - }, - ]} - /> + )} @@ -264,20 +271,21 @@ const InstalledSoftware = ({ labels, spec, onDeleteOs, onDeleteApp, onEdit, canE const imageMatches = refUri === (appSpec as ContainerApplication).image; return imageMatches && v.channels.includes(appChannel); }); - const actions: IAction[] = [ - ...(itemVersion - ? [ - { - title: t('Edit'), - onClick: () => onEdit(app.item.metadata.catalog, app.item.metadata.name || '', app.name), - }, - ] - : []), + const regularActions: IAction[] = itemVersion + ? [ + { + title: t('Edit'), + onClick: () => onEdit(app.item.metadata.catalog, app.item.metadata.name || '', app.name), + }, + ] + : []; + const dangerActions: IAction[] = [ { title: t('Delete'), onClick: () => setAppToDelete(app.name), }, ]; + const actions = buildAllDropdownActions(regularActions, dangerActions); return ( diff --git a/libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx b/libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx index 0f0142608..d2c4af666 100644 --- a/libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx +++ b/libs/ui-components/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Trans } from 'react-i18next'; -import { Button, Divider, DropdownItem, DropdownList, Tab } from '@patternfly/react-core'; +import { Button, DropdownItem, Tab } from '@patternfly/react-core'; import { Device, @@ -39,6 +39,7 @@ import { YamlEditorLoader } from '../../common/CodeEditor/YamlEditor'; import DeviceAliasEdit from './DeviceAliasEdit'; import { SystemRestoreBanners } from '../../SystemRestore/SystemRestoreBanners'; import DeviceDetailsCatalog from './DeviceDetailsCatalog'; +import ActionsDropdownList from '../../common/ActionsDropdownList'; type DeviceDetailsPageProps = React.PropsWithChildren<{ hideTerminal?: boolean }>; @@ -180,20 +181,21 @@ const DeviceDetailsPage = ({ children, hideTerminal }: DeviceDetailsPageProps) = actions={ isEnrolled ? ( - + {hasEditPermissions && ( - navigate({ route: ROUTE.DEVICE_EDIT, postfix: deviceId })} - {...editActionProps} - > - {t('Edit device configurations')} - + + navigate({ route: ROUTE.DEVICE_EDIT, postfix: deviceId })} + {...editActionProps} + > + {t('Edit device configurations')} + + )} - {canResume && resumeAction} - {canDecommission && (hasEditPermissions || canResume) && } - {canDecommission && decommissionAction} - + {canResume && {resumeAction}} + {canDecommission && {decommissionAction}} + ) : ( canDelete && ( diff --git a/libs/ui-components/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx b/libs/ui-components/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx index f6d7f2a5e..051cd54a8 100644 --- a/libs/ui-components/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx +++ b/libs/ui-components/src/components/Device/DevicesPage/DecommissionedDeviceTableRow.tsx @@ -6,6 +6,7 @@ import { ListAction } from '../../ListPage/types'; import { useTranslation } from '../../../hooks/useTranslation'; import { ROUTE, useNavigate } from '../../../hooks/useNavigate'; import ResourceLink from '../../common/ResourceLink'; +import { buildAllDropdownActions } from '../../common/ActionsDropdownList'; import DeviceLifecycleStatus from '../../Status/DeviceLifecycleStatus'; type DecommissionedDeviceTableRowProps = { @@ -33,6 +34,34 @@ const DecommissionedDeviceTableRow = ({ const deviceName = device.metadata.name as string; const deviceAlias = device.metadata.labels?.alias; + const regularActions: IAction[] = [ + ...(canEdit + ? [ + { + title: t('Edit device configurations'), + onClick: () => navigate({ route: ROUTE.DEVICE_EDIT, postfix: deviceName }), + isAriaDisabled: true, + tooltipProps: { + content: t('Device already started decommissioning and cannot be edited.'), + }, + }, + ] + : []), + { + title: t('View device details'), + onClick: () => navigate({ route: ROUTE.DEVICE_DETAILS, postfix: deviceName }), + }, + ]; + const dangerActions: IAction[] = canDelete + ? [ + deleteAction({ + resourceId: deviceName, + resourceName: deviceAlias, + }), + ] + : []; + const actionItems = buildAllDropdownActions(regularActions, dangerActions); + return ( {canDelete && ( - navigate({ route: ROUTE.DEVICE_EDIT, postfix: deviceName }), - isAriaDisabled: true, - tooltipProps: { - content: t('Device already started decommissioning and cannot be edited.'), - }, - }, - ] - : []), - { - title: t('View device details'), - onClick: () => navigate({ route: ROUTE.DEVICE_DETAILS, postfix: deviceName }), - }, - ...(canDelete - ? [ - { isSeparator: true } as IAction, - deleteAction({ - resourceId: deviceName, - resourceName: deviceAlias, - }), - ] - : []), - ]} - /> + )} diff --git a/libs/ui-components/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx b/libs/ui-components/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx index 04c2ddc24..4d43a6f5b 100644 --- a/libs/ui-components/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx +++ b/libs/ui-components/src/components/Device/DevicesPage/EnrolledDeviceTableRow.tsx @@ -12,6 +12,7 @@ import SystemUpdateStatus from '../../Status/SystemUpdateStatus'; import { useTranslation } from '../../../hooks/useTranslation'; import { ROUTE, useNavigate } from '../../../hooks/useNavigate'; import ResourceLink from '../../common/ResourceLink'; +import { buildAllDropdownActions } from '../../common/ActionsDropdownList'; import { ApiTableColumn } from '../../Table/Table'; type EnrolledDeviceTableRowProps = { @@ -53,7 +54,7 @@ const EnrolledDeviceTableRow = ({ const columnIds = React.useMemo(() => deviceColumns.map(({ id }) => id), [deviceColumns]); - const actionItems: IAction[] = [ + const regularActions: IAction[] = [ ...(canEdit ? [ { @@ -78,17 +79,18 @@ const EnrolledDeviceTableRow = ({ }), ] : []), - ...(canDecommission && decommissionAction + ]; + const dangerActions: IAction[] = + canDecommission && decommissionAction ? [ - { isSeparator: true } as IAction, decommissionAction({ resourceId: deviceName, resourceName: deviceAlias, disabledReason: decommissionDisabledReason, }), ] - : []), - ]; + : []; + const actionItems = buildAllDropdownActions(regularActions, dangerActions); return ( diff --git a/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestDetails/EnrollmentRequestDetails.tsx b/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestDetails/EnrollmentRequestDetails.tsx index 47f0dc597..ac51d5713 100644 --- a/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestDetails/EnrollmentRequestDetails.tsx +++ b/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestDetails/EnrollmentRequestDetails.tsx @@ -7,9 +7,7 @@ import { DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, - Divider, DropdownItem, - DropdownList, Grid, GridItem, TextArea, @@ -38,6 +36,7 @@ import { useAppContext } from '../../../hooks/useAppContext'; import { usePermissionsContext } from '../../common/PermissionsContext'; import { RESOURCE, VERB } from '../../../types/rbac'; import PageWithPermissions from '../../common/PageWithPermissions'; +import ActionsDropdownList from '../../common/ActionsDropdownList'; import './EnrollmentRequestDetails.css'; @@ -87,15 +86,16 @@ const EnrollmentRequestDetails = () => { actions={ (canApprove || canDelete) && ( - + {canApprove && ( - setIsApprovalModalOpen(true)} isDisabled={!isPendingApproval}> - {t('Approve')} - + + setIsApprovalModalOpen(true)} isDisabled={!isPendingApproval}> + {t('Approve')} + + )} - {canDelete && canApprove && } - {canDelete && deleteAction} - + {canDelete && {deleteAction}} + ) } diff --git a/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestTableRow.tsx b/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestTableRow.tsx index 3cca3538e..634d816d0 100644 --- a/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestTableRow.tsx +++ b/libs/ui-components/src/components/EnrollmentRequest/EnrollmentRequestTableRow.tsx @@ -8,6 +8,7 @@ import { ListAction } from '../ListPage/types'; import { useTranslation } from '../../hooks/useTranslation'; import { ROUTE } from '../../hooks/useNavigate'; import ResourceLink from '../common/ResourceLink'; +import { buildAllDropdownActions } from '../common/ActionsDropdownList'; type EnrollmentRequestTableRow = { rowIndex: number; @@ -38,19 +39,15 @@ const EnrollmentRequestTableRow: React.FC = ({ onApprove(erName); }; - const actionItems: IAction[] = []; + const regularActions: IAction[] = []; if (canApprove) { - actionItems.push({ + regularActions.push({ title: t('Approve'), onClick: approveEnrollment, }); } - if (canDelete) { - if (actionItems.length > 0) { - actionItems.push({ isSeparator: true } as IAction); - } - actionItems.push(deleteAction({ resourceId: erName })); - } + const dangerActions: IAction[] = canDelete ? [deleteAction({ resourceId: erName })] : []; + const actionItems = buildAllDropdownActions(regularActions, dangerActions); return ( diff --git a/libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx b/libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx index 591ab3e4f..82eb48635 100644 --- a/libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx +++ b/libs/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Divider, DropdownItem, DropdownList, Tab } from '@patternfly/react-core'; +import { DropdownItem, Tab } from '@patternfly/react-core'; import { Fleet } from '@flightctl/types'; import { RESOURCE, VERB } from '../../../types/rbac'; @@ -17,6 +17,7 @@ import FleetRestoreBanner from './FleetRestoreBanner'; import FleetYaml from './FleetYaml'; import TabsNav from '../../TabsNav/TabsNav'; import FleetDetailsCatalog from './FleetDetailsCatalog'; +import ActionsDropdownList from '../../common/ActionsDropdownList'; const fleetDetailsPermissions = [ { kind: RESOURCE.FLEET, verb: VERB.DELETE }, @@ -63,46 +64,50 @@ const FleetDetailPage = () => { actions={ hasActions && ( - + {isManaged && ( - navigate({ route: ROUTE.FLEET_EDIT, postfix: fleetId })} - > - {t('View fleet configurations')} - + + navigate({ route: ROUTE.FLEET_EDIT, postfix: fleetId })} + > + {t('View fleet configurations')} + + )} {canEdit && !isManaged && ( - navigate({ route: ROUTE.FLEET_EDIT, postfix: fleetId })} - > - {t('Edit fleet configurations')} - + + navigate({ route: ROUTE.FLEET_EDIT, postfix: fleetId })} + > + {t('Edit fleet configurations')} + + )} - - {canDelete && (isManaged || canEdit) && } {canDelete && ( - { - setIsDeleteModalOpen(true); - }} - isAriaDisabled={isManaged} - tooltipProps={ - isManaged - ? { - content: t( - "This fleet is managed by a resource sync and cannot be directly deleted. Either remove this fleet's definition from the resource sync configuration, or delete the resource sync first.", - ), - } - : undefined - } - > - {t('Delete fleet')} - + + { + setIsDeleteModalOpen(true); + }} + isAriaDisabled={isManaged} + tooltipProps={ + isManaged + ? { + content: t( + "This fleet is managed by a resource sync and cannot be directly deleted. Either remove this fleet's definition from the resource sync configuration, or delete the resource sync first.", + ), + } + : undefined + } + > + {t('Delete fleet')} + + )} - + ) } diff --git a/libs/ui-components/src/components/Fleet/FleetRow.tsx b/libs/ui-components/src/components/Fleet/FleetRow.tsx index 328e9fff9..0b37c1f3c 100644 --- a/libs/ui-components/src/components/Fleet/FleetRow.tsx +++ b/libs/ui-components/src/components/Fleet/FleetRow.tsx @@ -8,6 +8,7 @@ import { getFleetRolloutStatusWarning } from '../../utils/status/fleet'; import { getOwnerName } from '../../utils/resource'; import ResourceLink from '../common/ResourceLink'; +import { buildAllDropdownActions } from '../common/ActionsDropdownList'; import FleetDevicesCount from './FleetDetails/FleetDevicesCount'; import { FleetOwnerLinkIcon } from './FleetDetails/FleetOwnerLink'; import FleetStatus from './FleetStatus'; @@ -61,26 +62,27 @@ const FleetRow: React.FC = ({ const fleetName = fleet.metadata.name || ''; const isManaged = !!fleet.metadata?.owner; - const actions = useFleetActions(fleetName, isManaged, canEdit); + const regularActions = useFleetActions(fleetName, isManaged, canEdit); + const dangerActions: IAction[] = canDelete + ? [ + { + title: t('Delete fleet'), + 'data-testid': 'fleet-row-menu-delete-fleet', + onClick: onDeleteClick, + tooltipProps: isManaged + ? { + content: t( + "This fleet is managed by a resource sync and cannot be directly deleted. Either remove this fleet's definition from the resource sync configuration, or delete the resource sync first.", + ), + } + : undefined, + isAriaDisabled: isManaged, + } as IAction, + ] + : []; + const actions = buildAllDropdownActions(regularActions, dangerActions); const fleetRolloutError = getFleetRolloutStatusWarning(fleet, t); - if (canDelete) { - actions.push({ isSeparator: true } as IAction); - actions.push({ - title: t('Delete fleet'), - 'data-testid': 'fleet-row-menu-delete-fleet', - onClick: onDeleteClick, - tooltipProps: isManaged - ? { - content: t( - "This fleet is managed by a resource sync and cannot be directly deleted. Either remove this fleet's definition from the resource sync configuration, or delete the resource sync first.", - ), - } - : undefined, - isAriaDisabled: isManaged, - } as IAction); - } - return ( { actions={ (canPromote || canNewVersion || canDelete || canCancel) && ( - + {canNewVersion && ( - navigate({ route: ROUTE.IMAGE_BUILD_NEW_VERSION, postfix: imageBuildId })}> - {t('Rebuild')} - + + navigate({ route: ROUTE.IMAGE_BUILD_NEW_VERSION, postfix: imageBuildId })} + > + {t('Rebuild')} + + )} {canPromote && ( - setIsImagePromotionOpen(true)}>{t('Add to catalog')} + + setIsImagePromotionOpen(true)}>{t('Add to catalog')} + )} - {(canCancel || (canDelete && !canCancel)) && (canNewVersion || canPromote) && } {canCancel && ( - setIsCancelModalOpen(true)}>{t('Cancel image build')} + + setIsCancelModalOpen(true)}>{t('Cancel image build')} + )} {canDelete && !canCancel && ( - setIsDeleteModalOpen(true)}>{t('Delete image build')} + + setIsDeleteModalOpen(true)}>{t('Delete image build')} + )} - + ) } diff --git a/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsTab.tsx b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsTab.tsx index b70a0daed..a68b1f7b2 100644 --- a/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsTab.tsx +++ b/libs/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsTab.tsx @@ -41,6 +41,7 @@ import TablePagination from '../../Table/TablePagination'; import { getResourceId } from '../../../utils/resource'; import DeleteImagePromotionModal from '../../ImagePromotion/DeleteImagePromotionModal'; import { usePermissionsContext } from '../../common/PermissionsContext'; +import { buildAllDropdownActions } from '../../common/ActionsDropdownList'; import { RESOURCE, VERB } from '../../../types/rbac'; import { useImagePromotionsContext } from '../ImagePromotionsContext'; @@ -65,24 +66,23 @@ const ImagePromotionRow = ({ }) => { const { t } = useTranslation(); - const actions: IAction[] = [ - ...(canEditPromotions - ? [ - { - title: t('Edit image promotion'), - onClick: onEditClick, - }, - ] - : []), - ...(canDeletePromotions - ? [ - { - title: t('Delete image promotion'), - onClick: onDeleteClick, - }, - ] - : []), - ]; + const regularActions: IAction[] = canEditPromotions + ? [ + { + title: t('Edit image promotion'), + onClick: onEditClick, + }, + ] + : []; + const dangerActions: IAction[] = canDeletePromotions + ? [ + { + title: t('Delete image promotion'), + onClick: onDeleteClick, + }, + ] + : []; + const actions = buildAllDropdownActions(regularActions, dangerActions); return ( diff --git a/libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx b/libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx index b16a00126..26ed63fba 100644 --- a/libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx +++ b/libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx @@ -11,6 +11,7 @@ import { ROUTE, useNavigate } from '../../hooks/useNavigate'; import { getImageBuildImage, getImageBuildStatusReason, isImageBuildCancelable } from '../../utils/imageBuilds'; import { getDateDisplay } from '../../utils/dates'; import ResourceLink from '../common/ResourceLink'; +import { buildAllDropdownActions } from '../common/ActionsDropdownList'; import ImageBuildExportsGallery from './ImageBuildDetails/ImageBuildExportsGallery'; import { ImageBuildStatusDisplay } from './ImageBuildAndExportStatus'; @@ -54,7 +55,7 @@ const ImageBuildRow = ({ const imageBuildName = imageBuild.metadata.name || ''; const buildReason = getImageBuildStatusReason(imageBuild); - const actions: IAction[] = [ + const regularActions: IAction[] = [ { title: t('View details'), onClick: () => { @@ -64,32 +65,32 @@ const ImageBuildRow = ({ ]; if (canNewVersion) { - actions.push({ + regularActions.push({ title: t('Rebuild'), onClick: onNewVersionClick, }); } if (canAddToCatalog) { - actions.push({ + regularActions.push({ title: t('Add to catalog'), onClick: onAddToCatalog, }); } + const dangerActions: IAction[] = []; if (canCancel && isImageBuildCancelable(buildReason)) { - actions.push({ isSeparator: true } as IAction); - actions.push({ + dangerActions.push({ title: t('Cancel image build'), onClick: onCancelClick, }); } else if (canDelete) { - actions.push({ isSeparator: true } as IAction); - actions.push({ + dangerActions.push({ title: t('Delete image build'), onClick: onDeleteClick, }); } + const actions = buildAllDropdownActions(regularActions, dangerActions); const sourceImage = getImageBuildImage(imageBuild.spec.source); const destinationImage = getImageBuildImage(imageBuild.spec.destination); diff --git a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx index 7a7a76058..6cbea73e9 100644 --- a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Divider, DropdownItem, DropdownList, Grid, GridItem, Tab } from '@patternfly/react-core'; +import { DropdownItem, Grid, GridItem, Tab } from '@patternfly/react-core'; import { useFetchPeriodically } from '../../../hooks/useFetchPeriodically'; import { RepoSpecType, Repository, ResourceKind } from '@flightctl/types'; @@ -18,6 +18,7 @@ import PageWithPermissions from '../../common/PageWithPermissions'; import YamlEditor from '../../common/CodeEditor/YamlEditor'; import EventsCard from '../../Events/EventsCard'; import TabsNav from '../../TabsNav/TabsNav'; +import ActionsDropdownList from '../../common/ActionsDropdownList'; const repositoryDetailsPermissions = [ { kind: RESOURCE.REPOSITORY, verb: VERB.DELETE }, @@ -56,17 +57,20 @@ const RepositoryDetails = () => { actions={ (canDelete || canEdit) && ( - + {canEdit && ( - navigate({ route: ROUTE.REPO_EDIT, postfix: repositoryId })}> - {t('Edit repository')} - + + navigate({ route: ROUTE.REPO_EDIT, postfix: repositoryId })}> + {t('Edit repository')} + + )} - {canDelete && canEdit && } {canDelete && ( - setIsDeleteModalOpen(true)}>{t('Delete repository')} + + setIsDeleteModalOpen(true)}>{t('Delete repository')} + )} - + ) } diff --git a/libs/ui-components/src/components/Repository/RepositoryList.tsx b/libs/ui-components/src/components/Repository/RepositoryList.tsx index dc7a35639..2b0a704d7 100644 --- a/libs/ui-components/src/components/Repository/RepositoryList.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryList.tsx @@ -26,6 +26,7 @@ import ResourceListEmptyState from '../common/ResourceListEmptyState'; import { useTranslation } from '../../hooks/useTranslation'; import { ROUTE, useNavigate } from '../../hooks/useNavigate'; import ResourceLink from '../common/ResourceLink'; +import { buildAllDropdownActions } from '../common/ActionsDropdownList'; import RepositoryStatus from '../Status/RepositoryStatus'; import PageWithPermissions from '../common/PageWithPermissions'; import { RESOURCE, VERB } from '../../types/rbac'; @@ -105,25 +106,25 @@ const RepositoryTableRow = ({ const { t } = useTranslation(); const navigate = useNavigate(); - const actions: IAction[] = []; + const regularActions: IAction[] = []; const repoName = repository.metadata.name as string; if (canEdit) { - actions.push({ + regularActions.push({ title: t('Edit repository'), onClick: () => navigate({ route: ROUTE.REPO_EDIT, postfix: repository.metadata.name }), 'data-testid': 'repository-row-menu-edit-repository', } as IAction); } - if (canDelete) { - if (actions.length > 0) { - actions.push({ isSeparator: true } as IAction); - } - actions.push({ - title: t('Delete repository'), - onClick: () => setDeleteModalRepoId(repository.metadata.name), - 'data-testid': 'repository-row-menu-delete-repository', - } as IAction); - } + const dangerActions: IAction[] = canDelete + ? [ + { + title: t('Delete repository'), + onClick: () => setDeleteModalRepoId(repository.metadata.name), + 'data-testid': 'repository-row-menu-delete-repository', + } as IAction, + ] + : []; + const actions = buildAllDropdownActions(regularActions, dangerActions); return ( ) => ( + <>{children} +); +ActionsDropdownListItem.displayName = 'ActionsDropdownListItem'; + +const ActionsDropdownList = ({ children }: React.PropsWithChildren) => { + const actions: React.ReactNode[] = []; + const dangerActions: React.ReactNode[] = []; + + React.Children.forEach(children, (child) => { + if (!child) { + return; + } + if (React.isValidElement(child) && child.type === ActionsDropdownListItem) { + if (child.props.isDanger) { + dangerActions.push(child); + } else { + actions.push(child); + } + return; + } + actions.push(child); + }); + + const showDivider = actions.length > 0 && dangerActions.length > 0; + + return ( + + {actions} + {showDivider && } + {dangerActions} + + ); +}; + +ActionsDropdownList.Item = ActionsDropdownListItem; + +export const buildAllDropdownActions = (actions: IAction[], dangerActions: IAction[]): IAction[] => { + if (actions.length === 0) { + return dangerActions; + } + if (dangerActions.length === 0) { + return actions; + } + return [...actions, { isSeparator: true } as IAction, ...dangerActions]; +}; + +export default ActionsDropdownList; From 057963bc34ef7b5332691b4b8f630dd11eb1af86 Mon Sep 17 00:00:00 2001 From: Celia Amador Date: Wed, 17 Jun 2026 12:41:24 +0200 Subject: [PATCH 3/4] Addressing image export actions Made-with: Cursor --- .../ImageBuilds/ImageExportCards.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx b/libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx index 32ac06756..4b93fb19b 100644 --- a/libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx +++ b/libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx @@ -13,7 +13,6 @@ import { ContentVariants, Dropdown, DropdownItem, - DropdownList, Flex, FlexItem, Icon, @@ -33,6 +32,7 @@ import { getExportFormatDescription, getExportFormatLabel, getImageExportStatusR import { getDateDisplay } from '../../utils/dates'; import { useTranslation } from '../../hooks/useTranslation'; import WithTooltip from '../common/WithTooltip'; +import ActionsDropdownList from '../common/ActionsDropdownList'; import ConfirmImageExportActionModal, { ConfirmImageExportAction, } from './ConfirmImageExportModal/ConfirmImageExportModal'; @@ -60,7 +60,7 @@ const getActionsForStatus = ( if (format !== ExportFormatType.ExportFormatTypeQCOW2DiskContainer) { actions.push('download'); } - actions.push('viewLogs', 'delete', 'rebuild'); + actions.push('viewLogs', 'rebuild', 'delete'); break; case ImageExportConditionReason.ImageExportConditionReasonFailed: case ImageExportConditionReason.ImageExportConditionReasonCanceled: @@ -228,17 +228,18 @@ export const ViewImageBuildExportCard = ({ const isLoading = exportAction === activeAction; return ( - { - setActionsDropdownOpen(false); - handleCardAction(exportAction); - }} - isDisabled={isDisabled} - isLoading={isLoading} - > - {getActionTitle(t, exportAction, false)} - + + { + setActionsDropdownOpen(false); + handleCardAction(exportAction); + }} + isDisabled={isDisabled} + isLoading={isLoading} + > + {getActionTitle(t, exportAction, false)} + + ); }; @@ -310,7 +311,7 @@ export const ViewImageBuildExportCard = ({ )} > - {remainingActions.map((actionKey) => renderDropdownItem(actionKey))} + {remainingActions.map(renderDropdownItem)} )} From b6bb5cabec802af197409bd3399002f1ea8cc7f9 Mon Sep 17 00:00:00 2001 From: "bugs-buddy-jira-ai-issue-solver[bot]" <252868304+bugs-buddy-jira-ai-issue-solver[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:50:00 +0000 Subject: [PATCH 4/4] EDM-4157: address PR feedback Co-authored-by: Celia Amador Gonzalez --- .../src/components/common/ActionsDropdownList.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libs/ui-components/src/components/common/ActionsDropdownList.tsx b/libs/ui-components/src/components/common/ActionsDropdownList.tsx index 89ec59332..bca73a7da 100644 --- a/libs/ui-components/src/components/common/ActionsDropdownList.tsx +++ b/libs/ui-components/src/components/common/ActionsDropdownList.tsx @@ -6,11 +6,22 @@ type ActionsDropdownListItemProps = { isDanger?: boolean; }; +/** + * Transparent wrapper whose only purpose is to carry the `isDanger` prop. + * The prop is never consumed here — `ActionsDropdownList` reads it via + * `child.props.isDanger` to classify children into regular vs. danger groups. + */ const ActionsDropdownListItem = ({ children }: React.PropsWithChildren) => ( <>{children} ); ActionsDropdownListItem.displayName = 'ActionsDropdownListItem'; +/** + * Dropdown list that automatically inserts a divider between regular and danger actions. + * + * Children are classified by reference-equality check (`child.type === ActionsDropdownListItem`). + * Wrap each action in `` and set `isDanger` on destructive ones. + */ const ActionsDropdownList = ({ children }: React.PropsWithChildren) => { const actions: React.ReactNode[] = []; const dangerActions: React.ReactNode[] = [];