Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,7 @@
"Export images": "Export images",
"Add to catalog": "Add to catalog",
"Delete image build": "Delete image build",
"View image promotion": "View image promotion",
"Edit image promotion": "Edit image promotion",
"Delete image promotion": "Delete image promotion",
"Target catalog": "Target catalog",
Expand Down Expand Up @@ -1463,17 +1464,24 @@
"Failed to load catalogs": "Failed to load catalogs",
"Failed to load catalog items": "Failed to load catalog items",
"Image Promotion name": "Image Promotion name",
"Formats": "Formats",
"Remove {{ exportLabel }}": "Remove {{ exportLabel }}",
"Add {{ exportLabel }}": "Add {{ exportLabel }}",
"No additional formats": "No additional formats",
"Add as": "Add as",
"Existing catalog item": "Existing catalog item",
"New catalog item": "New catalog item",
"No additional formats": "No additional formats",
"No formats selected": "No formats selected",
"Formats": "Formats",
"Remove {{ exportLabel }}": "Remove {{ exportLabel }}",
"Add {{ exportLabel }}": "Add {{ exportLabel }}",
"Add build to catalog": "Add build to catalog",
"Failed to update ImagePromotion": "Failed to update ImagePromotion",
"Failed to create ImagePromotion": "Failed to create ImagePromotion",
"Failed to load Image promotions": "Failed to load Image promotions",
"You do not have permissions to update image promotions": "You do not have permissions to update image promotions",
"Image promotion cannot be edited while publishing is in progress": "Image promotion cannot be edited while publishing is in progress",
"Failed image promotions cannot be edited. Create a new image promotion to retry.": "Failed image promotions cannot be edited. Create a new image promotion to retry.",
"Image promotions for failed image builds cannot be edited. Create a new image promotion to retry.": "Image promotions for failed image builds cannot be edited. Create a new image promotion to retry.",
"Image promotions for canceled image builds cannot be edited. Create a new image promotion to retry.": "Image promotions for canceled image builds cannot be edited. Create a new image promotion to retry.",
"All export formats from this image build are already included in this promotion": "All export formats from this image build are already included in this promotion",
"Catalog item is required": "Catalog item is required",
"Select or create repository": "Select or create repository",
"Add resource sync": "Add resource sync",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ import {
Stack,
StackItem,
} from '@patternfly/react-core';
import { ActionsColumn, IAction, Tbody, Td, Tr } from '@patternfly/react-table';
import { TFunction, Trans } from 'react-i18next';
import { InfoCircleIcon } from '@patternfly/react-icons/dist/js/icons/info-circle-icon';

import { BindingType, ImagePromotion } from '@flightctl/types/imagebuilder';
import { BindingType, ImageExport, ImagePromotion } from '@flightctl/types/imagebuilder';
import ImagePromotionModal from '../../ImagePromotion/ImagePromotionModal';
import { getPromotionEditDisabledReason } from '../../ImagePromotion/utils';
import { getDisabledTooltipProps } from '../../../utils/tooltip';
import ImagePromotionStatus from '../../ImagePromotion/ImagePromotionStatus';
import { getDateDisplay } from '../../../utils/dates';
import { getExportFormatLabel, getImageReference } from '../../../utils/imageBuilds';
Expand All @@ -36,7 +39,6 @@ import { ImageBuildStatusDisplay } from '../ImageBuildAndExportStatus';
import ImageUrl from '../ImageUrl';
import { getErrorMessage } from '../../../utils/error';
import Table from '../../Table/Table';
import { ActionsColumn, IAction, Tbody, Td, Tr } from '@patternfly/react-table';
import TablePagination from '../../Table/TablePagination';
import { getResourceId } from '../../../utils/resource';
import DeleteImagePromotionModal from '../../ImagePromotion/DeleteImagePromotionModal';
Expand All @@ -51,38 +53,53 @@ const detailsPermissions = [
{ kind: RESOURCE.IMAGE_PROMOTION, verb: VERB.DELETE },
];

type PromotionAction = 'view' | 'edit' | 'delete';

type PromotionActionState = {
id: string;
action: PromotionAction;
};

const ImagePromotionRow = ({
imagePromotion,
onDeleteClick,
onEditClick,
editDisabledReason,
onActionClick,
canEditPromotions,
canDeletePromotions,
}: {
imagePromotion: ImagePromotion;
onDeleteClick: VoidFunction;
onEditClick: VoidFunction;
editDisabledReason?: string;
onActionClick: (action: PromotionAction) => void;
canEditPromotions: boolean;
canDeletePromotions: boolean;
}) => {
const { t } = useTranslation();

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);
const actions: IAction[] = [
{
title: t('View image promotion'),
onClick: () => onActionClick('view'),
},
];
if (canEditPromotions) {
actions.push({
title: t('Edit image promotion'),
onClick: () => onActionClick('edit'),
...getDisabledTooltipProps(editDisabledReason),
});
}

const allActions = buildAllDropdownActions(
actions,
canDeletePromotions
? [
{
title: t('Delete image promotion'),
onClick: () => onActionClick('delete'),
},
]
: [],
);

return (
<Tr>
Expand All @@ -93,11 +110,9 @@ const ImagePromotionRow = ({
<Td dataLabel={t('Status')}>
<ImagePromotionStatus promotion={imagePromotion} />
</Td>
{!!actions.length && (
<Td isActionCell>
<ActionsColumn items={actions} />
</Td>
)}
<Td isActionCell>
<ActionsColumn items={allActions} />
</Td>
</Tr>
);
};
Expand Down Expand Up @@ -130,8 +145,9 @@ const ImagePromotionsCard = ({

const { error, isLoading, isUpdating, pagination, imagePromotions, refetchPromotions } = useImagePromotionsContext();

const [promotionToDeleteId, setPromotionToDeleteId] = React.useState<string>();
const [promotionToEditId, setPromotionToEditId] = React.useState<string>();
const [promotionAction, setPromotionAction] = React.useState<PromotionActionState>();

const availableFormats = imageBuild.imageExports.filter(Boolean).map((e) => (e as ImageExport).spec.format);

let content: React.ReactNode;
if (error) {
Expand All @@ -153,8 +169,8 @@ const ImagePromotionsCard = ({
<ImagePromotionRow
key={getResourceId(promotion)}
imagePromotion={promotion}
onEditClick={() => setPromotionToEditId(promotion.metadata.name)}
onDeleteClick={() => setPromotionToDeleteId(promotion.metadata.name)}
editDisabledReason={getPromotionEditDisabledReason(promotion, availableFormats, canEditPromotions, t)}
onActionClick={(action) => setPromotionAction({ id: promotion.metadata.name as string, action })}
canEditPromotions={canEditPromotions}
canDeletePromotions={canDeletePromotions}
/>
Expand All @@ -166,30 +182,41 @@ const ImagePromotionsCard = ({
);
}

const promotionToDelete = imagePromotions.find((p) => p.metadata.name === promotionToDeleteId);
const promotionToEdit = imagePromotions.find((p) => p.metadata.name === promotionToEditId);
const activePromotion = promotionAction
? imagePromotions.find((p) => p.metadata.name === promotionAction.id)
: undefined;

const closePromotionAction = () => setPromotionAction(undefined);

return (
<DetailsPageCard>
<CardTitle>{t('Image promotions')}</CardTitle>
<CardBody>{content}</CardBody>
{promotionToDelete && (
{activePromotion && promotionAction?.action === 'delete' && (
<DeleteImagePromotionModal
promotion={promotionToDelete}
promotion={activePromotion}
onClose={(hasDeleted?: boolean) => {
if (hasDeleted) {
refetchPromotions();
}
setPromotionToDeleteId(undefined);
closePromotionAction();
}}
/>
)}
{promotionToEdit && (
{activePromotion && promotionAction?.action === 'view' && (
<ImagePromotionModal
readOnly
imageBuild={imageBuild}
imagePromotion={activePromotion}
onClose={closePromotionAction}
/>
)}
{activePromotion && promotionAction?.action === 'edit' && (
<ImagePromotionModal
imageBuild={imageBuild}
imagePromotion={promotionToEdit}
imagePromotion={activePromotion}
onClose={(updated) => {
setPromotionToEditId(undefined);
closePromotionAction();
if (updated) {
refetchPromotions();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Alert, Button, FormGroup, FormSection, Spinner, Stack, StackItem } from '@patternfly/react-core';
import { Alert, FormGroup, FormSection, Spinner } from '@patternfly/react-core';
import { useFormikContext } from 'formik';

import { CatalogItem, CatalogList } from '@flightctl/types/alpha';
Expand All @@ -15,8 +15,8 @@ import NameField from '../form/NameField';
import { getDnsSubdomainValidations } from '../form/validations';
import { useCatalogItems } from '../Catalog/useCatalogs';
import { getErrorMessage } from '../../utils/error';
import { getExportFormatLabel } from '../../utils/imageBuilds';
import { ImagePromotionFormValues } from './types';
import ImagePromotionFormatsField from './ImagePromotionFormatsField';

const NewItemForm = ({ isDisabled }: { isDisabled?: boolean }) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -116,14 +116,16 @@ const ExistingItemForm = ({ catalogItems, isDisabled }: { catalogItems: CatalogI

const ImagePromotionForm = ({
isEdit,
canAmendExportFormats = true,
availableFormats,
}: {
isEdit?: boolean;
canAmendExportFormats?: boolean;
availableFormats?: ExportFormatType[];
}) => {
const { t } = useTranslation();

const { values, setFieldValue } = useFormikContext<ImagePromotionFormValues>();
const { values } = useFormikContext<ImagePromotionFormValues>();

const [catalogList, catalogsLoading, catalogsErr] = useFetchPeriodically<CatalogList>({
endpoint: 'catalogs',
Expand Down Expand Up @@ -173,10 +175,6 @@ const ImagePromotionForm = ({
);
}

const hasExports = availableFormats?.length || values.exportFormats.length;

const extraFormats = availableFormats?.filter((f) => !values.exportFormats.includes(f));

return (
<FlightCtlForm>
<NameField
Expand All @@ -187,37 +185,11 @@ const ImagePromotionForm = ({
isDisabled={isEdit}
validations={getDnsSubdomainValidations(t)}
/>
<FormGroup label={t('Formats')}>
{hasExports ? (
<Stack>
<StackItem>{values.exportFormats.map((format) => getExportFormatLabel(t, format)).join(', ')}</StackItem>
{extraFormats?.map((f) => {
const isIncluded = values.additionalExportFormats?.includes(f);
const exportLabel = getExportFormatLabel(t, f);
return (
<StackItem key={f}>
<Button
isInline
variant="link"
onClick={() => {
const newFormats = isIncluded
? values.additionalExportFormats?.filter((format) => format !== f)
: [...(values.additionalExportFormats || []), f];
setFieldValue('additionalExportFormats', newFormats);
}}
>
{isIncluded
? t('Remove {{ exportLabel }}', { exportLabel })
: t('Add {{ exportLabel }}', { exportLabel })}
</Button>
</StackItem>
);
})}
</Stack>
) : (
t('No additional formats')
)}
</FormGroup>
<ImagePromotionFormatsField
isEdit={isEdit}
canAmendExportFormats={canAmendExportFormats}
availableFormats={availableFormats}
/>
<FormGroup label={t('Catalog')} isRequired>
<FormSelect name="catalog" items={catalogs} isDisabled={isEdit} />
</FormGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react';
import { Button, FormGroup, Stack, StackItem } from '@patternfly/react-core';
import { useFormikContext } from 'formik';
import { ExportFormatType } from '@flightctl/types/imagebuilder';

import { useTranslation } from '../../hooks/useTranslation';
import { getExportFormatLabel } from '../../utils/imageBuilds';
import { ImagePromotionFormValues } from './types';

type ImagePromotionFormatsFieldProps = {
isEdit?: boolean;
canAmendExportFormats: boolean;
availableFormats?: ExportFormatType[];
};

const ImagePromotionFormatsField = ({
isEdit,
canAmendExportFormats = true,
availableFormats,
}: ImagePromotionFormatsFieldProps) => {
Comment on lines +10 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prop typed as required but given a default value — pick one.

canAmendExportFormats is typed as required (canAmendExportFormats: boolean;, no ?), yet the component destructures it with a default (= true). Since TypeScript forces every caller to pass the prop, that default can never actually run — it's dead code that also misleadingly implies the prop is optional. Either mark it optional (canAmendExportFormats?: boolean) to match the intended usage, or drop the default since callers must always supply it.

🔧 Suggested fix
 type ImagePromotionFormatsFieldProps = {
   isEdit?: boolean;
-  canAmendExportFormats: boolean;
+  canAmendExportFormats?: boolean;
   availableFormats?: ExportFormatType[];
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type ImagePromotionFormatsFieldProps = {
isEdit?: boolean;
canAmendExportFormats: boolean;
availableFormats?: ExportFormatType[];
};
const ImagePromotionFormatsField = ({
isEdit,
canAmendExportFormats = true,
availableFormats,
}: ImagePromotionFormatsFieldProps) => {
type ImagePromotionFormatsFieldProps = {
isEdit?: boolean;
canAmendExportFormats?: boolean;
availableFormats?: ExportFormatType[];
};
const ImagePromotionFormatsField = ({
isEdit,
canAmendExportFormats = true,
availableFormats,
}: ImagePromotionFormatsFieldProps) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@libs/ui-components/src/components/ImagePromotion/ImagePromotionFormatsField.tsx`
around lines 10 - 20, Make the `canAmendExportFormats` prop definition and
destructuring behavior consistent in `ImagePromotionFormatsField`: either mark
it optional to retain the `true` default, or remove the default and keep it
required. Prefer the option matching how existing callers use the component.

const { t } = useTranslation();
const { values, setFieldValue } = useFormikContext<ImagePromotionFormValues>();

const hasExports = availableFormats?.length || values.exportFormats.length;
const extraFormats = availableFormats?.filter((f) => !values.exportFormats.includes(f)) || [];
const emptyFormatsMessage = isEdit ? t('No additional formats') : t('No formats selected');

return (
<FormGroup label={t('Formats')}>
{hasExports ? (
<Stack>
<StackItem>{values.exportFormats.map((format) => getExportFormatLabel(t, format)).join(', ')}</StackItem>
{extraFormats?.map((f) => {
const isIncluded = values.additionalExportFormats?.includes(f);
const exportLabel = getExportFormatLabel(t, f);
return (
<StackItem key={f}>
<Button
isInline
variant="link"
isDisabled={isEdit && !canAmendExportFormats}
onClick={() => {
const newFormats = isIncluded
? values.additionalExportFormats?.filter((format) => format !== f)
: [...(values.additionalExportFormats || []), f];
setFieldValue('additionalExportFormats', newFormats);
}}
>
{isIncluded
? t('Remove {{ exportLabel }}', { exportLabel })
: t('Add {{ exportLabel }}', { exportLabel })}
</Button>
</StackItem>
);
})}
</Stack>
) : (
emptyFormatsMessage
)}
</FormGroup>
);
};

export default ImagePromotionFormatsField;
Loading
Loading