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
1 change: 0 additions & 1 deletion libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,6 @@
"Build image": "Build image",
"You do not have permissions to promote builds to the catalog": "You do not have permissions to promote builds to the catalog",
"Add to the software catalog testing channel upon successful build": "Add to the software catalog testing channel upon successful build",
"Repositories used for output images must be writable.": "Repositories used for output images must be writable.",
"Management-ready by default": "Management-ready by default",
"The agent is automatically included in this image. This ensures your devices are ready to be managed immediately after they are deployed.": "The agent is automatically included in this image. This ensures your devices are ready to be managed immediately after they are deployed.",
"Target repository": "Target repository",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import { Alert, Content, FormGroup, FormSection, Gallery } from '@patternfly/react-core';
import { FormikErrors, useFormikContext } from 'formik';

import { OciRepoSpec, RepoSpecType, Repository } from '@flightctl/types';
import { RepoSpecType } from '@flightctl/types';
import { ExportFormatType } from '@flightctl/types/imagebuilder';
import { ImageBuildFormValues } from '../types';
import { useTranslation } from '../../../../hooks/useTranslation';
Expand All @@ -13,7 +13,6 @@ import { usePermissionsContext } from '../../../common/PermissionsContext';
import { RESOURCE, VERB } from '../../../../types/rbac';
import { SelectImageBuildExportCard } from '../../ImageExportCards';
import { getAllExportFormats } from '../../../../utils/imageBuilds';
import { isOciRepoSpec } from '../../../Repository/CreateRepository/utils';
import { useOciRegistriesContext } from '../../OciRegistriesContext';
import ImageUrlCard from '../../ImageUrlCard';

Expand All @@ -34,16 +33,6 @@ const OutputImageStep = () => {
const { ociRegistries, refetch } = useOciRegistriesContext();
const [canCreateRepo] = checkPermissions([{ kind: RESOURCE.REPOSITORY, verb: VERB.CREATE }]);

const writableRepoValidation = React.useCallback(
(repo: Repository) => {
if (isOciRepoSpec(repo.spec) && repo.spec.accessMode === OciRepoSpec.accessMode.READ) {
return t('Repositories used for output images must be writable.');
}
return undefined;
},
[t],
);

const handleFormatToggle = (format: ExportFormatType, isChecked: boolean) => {
const currentFormats = values.exportFormats;
if (isChecked) {
Expand Down Expand Up @@ -75,7 +64,6 @@ const OutputImageStep = () => {
options={{
writeAccessOnly: true,
}}
validateRepoSelection={writableRepoValidation}
helperText={t(
'Only OCI-compliant registries are shown. Other repository types, such as Git or HTTP, are not supported for image builds.',
)}
Expand Down
13 changes: 13 additions & 0 deletions libs/ui-components/src/components/form/FormSelect.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
overflow: auto;
}

.fctl-form-select__menu .pf-v6-c-menu__item-main {
position: relative;
padding-inline-end: calc(var(--pf-t--global--icon--size--md) + var(--pf-v6-c-menu__item-main--ColumnGap));
}

/* Render the select checkmark with absolute position so it doesn't affect the alignment of the content */
.fctl-form-select__menu .pf-v6-c-menu__item-select-icon {
position: absolute;
inset-inline-end: 0;
top: 50%;
transform: translateY(-50%);
}

.fctl-form-select {
max-width: 30rem;
}
120 changes: 51 additions & 69 deletions libs/ui-components/src/components/form/RepositorySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import * as React from 'react';
import { useField, useFormikContext } from 'formik';
import {
Button,
Content,
ContentVariants,
Divider,
Flex,
FlexItem,
FormGroup,
Grid,
GridItem,
Icon,
MenuFooter,
SelectList,
Expand All @@ -31,8 +33,6 @@ export const getRepositoryItems = (
repositories: Repository[],
repoType: RepoSpecType,
selectedRepoName?: string,
// Returns an error message if the repository cannot be selected
validateRepoSelection?: (repo: Repository) => string | undefined,
) => {
const invalidRepoItems: Record<string, SelectItem> = {};
const validRepoItems: Record<string, SelectItem> = {};
Expand All @@ -42,66 +42,59 @@ export const getRepositoryItems = (
return repo.spec.type === repoType;
})
.forEach((repo) => {
const selectionError = validateRepoSelection ? validateRepoSelection(repo) : undefined;
const repoName = repo.metadata.name as string;
if (selectionError) {
invalidRepoItems[repoName] = {
label: repoName,
description: (
<Stack>
<StackItem>{getRepoUrlOrRegistry(repo.spec)}</StackItem>
<StackItem>{selectionError}</StackItem>
</Stack>
),
};
} else {
const accessibleCondition = repo.status?.conditions?.find((c) => c.type === ConditionType.RepositoryAccessible);
const isAccessible = accessibleCondition && accessibleCondition.status === ConditionStatus.ConditionStatusTrue;
const isInaccessible =
accessibleCondition && accessibleCondition.status === ConditionStatus.ConditionStatusFalse;
const urlOrRegistry = getRepoUrlOrRegistry(repo.spec);

let accessText = t('Unknown');
let level: StatusLevel = 'unknown';
if (isAccessible) {
accessText = t('Available');
level = 'success';
} else if (isInaccessible) {
accessText = t('Not available');
level = 'danger';
}

validRepoItems[repoName] = {
label: repoName,
description: (
<Flex
justifyContent={{ default: 'justifyContentSpaceBetween' }}
alignItems={{ default: 'alignItemsCenter' }}
>
<FlexItem>{urlOrRegistry}</FlexItem>
<FlexItem>
<StatusDisplayContent label={accessText} level={level} />
</FlexItem>
</Flex>
),
};
const accessibleCondition = repo.status?.conditions?.find((c) => c.type === ConditionType.RepositoryAccessible);
const isAccessible = accessibleCondition && accessibleCondition.status === ConditionStatus.ConditionStatusTrue;
const isInaccessible = accessibleCondition && accessibleCondition.status === ConditionStatus.ConditionStatusFalse;
const urlOrRegistry = getRepoUrlOrRegistry(repo.spec);

let accessText = t('Unknown');
let level: StatusLevel = 'unknown';
if (isAccessible) {
accessText = t('Available');
level = 'success';
} else if (isInaccessible) {
accessText = t('Not available');
level = 'danger';
}

validRepoItems[repoName] = {
label: (
<Grid hasGutter style={{ alignItems: 'center' }}>
<GridItem span={8}>
<Stack>
<StackItem>{repoName}</StackItem>
<StackItem>
<Content component={ContentVariants.small}>{urlOrRegistry}</Content>
</StackItem>
</Stack>
</GridItem>
<GridItem span={4}>
<StatusDisplayContent label={accessText} level={level} />
</GridItem>
</Grid>
),
selectedLabel: repoName,
};
});

// If the selected repository has been removed, we still consider it "valid" since it needs to be selected initially
const isSelectedRepoMissing =
selectedRepoName && !repositories.some((repo) => repo.metadata.name === selectedRepoName);
if (isSelectedRepoMissing && !validRepoItems[selectedRepoName]) {
validRepoItems[selectedRepoName] = {
label: selectedRepoName,
description: (
<>
<Icon size="sm" status="danger">
<ExclamationCircleIcon />
</Icon>{' '}
{t('Missing repository')}
</>
label: (
<Stack>
<StackItem>{selectedRepoName}</StackItem>
<StackItem>
<Icon size="sm" status="danger">
<ExclamationCircleIcon />
</Icon>{' '}
{t('Missing repository')}
</StackItem>
</Stack>
),
selectedLabel: selectedRepoName,
};
}

Expand All @@ -121,7 +114,6 @@ type RepositorySelectProps = {
writeAccessOnly?: boolean;
};
isRequired?: boolean;
validateRepoSelection?: (repo: Repository) => string | undefined;
};

const ReadOnlyRepositoryListItem = ({ invalidRepoItems }: { invalidRepoItems: Record<string, SelectItem> }) => {
Expand All @@ -134,7 +126,7 @@ const ReadOnlyRepositoryListItem = ({ invalidRepoItems }: { invalidRepoItems: Re
{itemKeys.map((key) => {
const item = invalidRepoItems[key];
return (
<SelectOption key={key} value={key} description={item.description} isDisabled>
<SelectOption key={key} value={key} isDisabled>
{item.label}
</SelectOption>
);
Expand All @@ -154,32 +146,22 @@ const RepositorySelect = ({
helperText,
options,
isRequired,
validateRepoSelection,
}: RepositorySelectProps) => {
const { t } = useTranslation();
const { setFieldValue, setFieldError } = useFormikContext();
const { setFieldValue } = useFormikContext();
const [field] = useField<string>(name);
const [createRepoModalOpen, setCreateRepoModalOpen] = React.useState(false);

const { validRepoItems, invalidRepoItems } = React.useMemo(() => {
return getRepositoryItems(t, repositories, repoType, field.value, validateRepoSelection);
}, [t, repositories, repoType, field.value, validateRepoSelection]);
return getRepositoryItems(t, repositories, repoType, field.value);
}, [t, repositories, repoType, field.value]);

const handleCreateRepository = (repo: Repository) => {
setCreateRepoModalOpen(false);
if (repoRefetch) {
repoRefetch();
}

// If the created repository cannot be selected, we set the error and skip marking the repository as selected
if (validateRepoSelection) {
const selectionError = validateRepoSelection(repo);
if (selectionError) {
setFieldError(name, selectionError);
return;
}
}

void setFieldValue(name, repo.metadata.name, true);
};

Expand Down
Loading