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 @@ -1277,6 +1277,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 @@ -1342,17 +1343,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 @@ -22,8 +22,10 @@ import {
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 @@ -50,40 +52,45 @@ 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,
canEditPromotions,
editDisabledReason,
onActionClick,
canDeletePromotions,
}: {
imagePromotion: ImagePromotion;
onDeleteClick: VoidFunction;
onEditClick: VoidFunction;
canEditPromotions: boolean;
editDisabledReason?: string;
onActionClick: (action: PromotionAction) => void;
canDeletePromotions: boolean;
}) => {
const { t } = useTranslation();

const actions: IAction[] = [
...(canEditPromotions
? [
{
title: t('Edit image promotion'),
onClick: onEditClick,
},
]
: []),
...(canDeletePromotions
? [
{
title: t('Delete image promotion'),
onClick: onDeleteClick,
},
]
: []),
{
title: t('View image promotion'),
onClick: () => onActionClick('view'),
},
{
title: t('Edit image promotion'),
onClick: () => onActionClick('edit'),
...getDisabledTooltipProps(editDisabledReason),
},
];

if (canDeletePromotions) {
actions.push({
title: t('Delete image promotion'),
onClick: () => onActionClick('delete'),
});
}

return (
<Tr>
<Td dataLabel={t('Name')}>{imagePromotion.metadata.name || ''}</Td>
Expand All @@ -93,11 +100,9 @@ const ImagePromotionRow = ({
<Td dataLabel={t('Status')}>
<ImagePromotionStatus promotion={imagePromotion} />
</Td>
{!!actions.length && (
<Td isActionCell>
<ActionsColumn items={actions} />
</Td>
)}
<Td isActionCell>
<ActionsColumn items={actions} />
</Td>
</Tr>
);
};
Expand Down Expand Up @@ -130,8 +135,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,9 +159,8 @@ const ImagePromotionsCard = ({
<ImagePromotionRow
key={getResourceId(promotion)}
imagePromotion={promotion}
onEditClick={() => setPromotionToEditId(promotion.metadata.name)}
onDeleteClick={() => setPromotionToDeleteId(promotion.metadata.name)}
canEditPromotions={canEditPromotions}
editDisabledReason={getPromotionEditDisabledReason(promotion, availableFormats, canEditPromotions, t)}
onActionClick={(action) => setPromotionAction({ id: promotion.metadata.name as string, action })}
canDeletePromotions={canDeletePromotions}
/>
))}
Expand All @@ -166,30 +171,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 FlightCtlForm from '../form/FlightCtlForm';
import { useTranslation } from '../../hooks/useTranslation';
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 @@ -110,14 +110,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 @@ -167,10 +169,6 @@ const ImagePromotionForm = ({
);
}

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

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

return (
<FlightCtlForm>
<NameField
Expand All @@ -181,37 +179,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) => {
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