From ed4894077fa6af82b13daa5a791aa58be41e868a Mon Sep 17 00:00:00 2001 From: Celia Amador Date: Mon, 29 Dec 2025 10:18:26 +0100 Subject: [PATCH 1/2] EDM-2913: Add support for OCI repositories --- libs/i18n/locales/en/translation.json | 31 +- libs/types/index.ts | 5 + libs/types/models/ApplicationVolume.ts | 2 + .../models/ApplicationVolumeReclaimPolicy.ts | 10 + libs/types/models/DockerAuth.ts | 19 ++ libs/types/models/K8sProviderSpec.ts | 4 + libs/types/models/OciAuth.ts | 10 + libs/types/models/OciAuthType.ts | 10 + libs/types/models/OciRepoSpec.ts | 50 ++++ libs/types/models/OpenShiftProviderSpec.ts | 8 + libs/types/models/RepoSpecType.ts | 1 + libs/types/models/RepositorySpec.ts | 3 +- .../ConfigWithRepositoryTemplateForm.tsx | 14 +- .../steps/RepositoryStep.tsx | 6 +- .../CreateRepository/CreateRepositoryForm.tsx | 164 ++++++++-- .../Repository/CreateRepository/types.ts | 14 +- .../Repository/CreateRepository/utils.ts | 281 +++++++++++++++--- .../RepositoryDetails/RepositoryDetails.tsx | 2 +- .../RepositoryGeneralDetailsCard.tsx | 41 ++- .../RepositorySourceList.tsx | 3 +- .../components/Repository/RepositoryList.tsx | 13 +- .../src/components/form/FormSelect.tsx | 2 +- 22 files changed, 594 insertions(+), 99 deletions(-) create mode 100644 libs/types/models/ApplicationVolumeReclaimPolicy.ts create mode 100644 libs/types/models/DockerAuth.ts create mode 100644 libs/types/models/OciAuth.ts create mode 100644 libs/types/models/OciAuthType.ts create mode 100644 libs/types/models/OciRepoSpec.ts diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 83d56c150..d60f6b929 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -928,30 +928,38 @@ "Failed to retrieve repository details": "Failed to retrieve repository details", "Failed to retrieve the resource syncs": "Failed to retrieve the resource syncs", "The repository cannot be modified at the moment because some of its details could not be obtained.": "The repository cannot be modified at the moment because some of its details could not be obtained.", + "Basic authentication": "Basic authentication", + "Username": "Username", + "Password": "Password", + "Skip server verification": "Skip server verification", + "CA certificate": "CA certificate", "HTTP": "HTTP", "SSH": "SSH", "Validation suffix": "Validation suffix", "Suffix to the repository's base URL used to validate if the HTTP service is accessible.": "Suffix to the repository's base URL used to validate if the HTTP service is accessible.", "Full validation URL: <1>{`${values.url}${values.validationSuffix || ''}`}": "Full validation URL: <1>{`${values.url}${values.validationSuffix || ''}`}", - "Basic authentication": "Basic authentication", - "Username": "Username", - "Password": "Password", "mTLS authentication": "mTLS authentication", "Client TLS certificate": "Client TLS certificate", "Client TLS key": "Client TLS key", - "Skip server verification": "Skip server verification", - "CA certificate": "CA certificate", "JWT authentication token for the HTTP service": "JWT authentication token for the HTTP service", "Token": "Token", "SSH private key": "SSH private key", "Private key passphrase": "Private key passphrase", "Use Git repository": "Use Git repository", "Use HTTP service": "Use HTTP service", + "Use OCI registry": "Use OCI registry", "Switching the repository type will cause some data to be lost.": "Switching the repository type will cause some data to be lost.", "Are you sure you want to change the repository type?": "Are you sure you want to change the repository type?", "Change": "Change", + "Registry hostname": "Registry hostname", + "For example: quay.io, registry.redhat.io, myregistry.com:5000": "For example: quay.io, registry.redhat.io, myregistry.com:5000", "Repository URL": "Repository URL", "For example: {{ demoRepositoryUrl }}": "For example: {{ demoRepositoryUrl }}", + "Scheme": "Scheme", + "HTTPS": "HTTPS", + "Access mode": "Access mode", + "Read only": "Read only", + "Read and write": "Read and write", "Use advanced configurations": "Use advanced configurations", "Use resource syncs": "Use resource syncs", "Resource sync name": "Resource sync name", @@ -963,16 +971,18 @@ "Add another resource sync": "Add another resource sync", "Target revision is required.": "Target revision is required.", "Must be an absolute path.": "Must be an absolute path.", - "Enter a valid repository URL. Example: {{ demoRepositoryUrl }}": "Enter a valid repository URL. Example: {{ demoRepositoryUrl }}", - "Repository URL is required": "Repository URL is required", - "Enter a valid HTTP service URL. Example: https://my-service-url": "Enter a valid HTTP service URL. Example: https://my-service-url", - "HTTP service URL is required": "HTTP service URL is required", "Repository type is required": "Repository type is required", "Username is required": "Username is required", "Password is required": "Password is required", "Client TLS certificate is required": "Client TLS certificate is required", "Client TLS key is required": "Client TLS key is required", "Must be a valid JWT token": "Must be a valid JWT token", + "Enter a valid registry hostname (e.g., quay.io, registry.redhat.io, myregistry.com:5000)": "Enter a valid registry hostname (e.g., quay.io, registry.redhat.io, myregistry.com:5000)", + "Registry hostname is required": "Registry hostname is required", + "Enter a valid repository URL. Example: {{ demoRepositoryUrl }}": "Enter a valid repository URL. Example: {{ demoRepositoryUrl }}", + "Repository URL is required": "Repository URL is required", + "Enter a valid HTTP service URL. Example: https://my-service-url": "Enter a valid HTTP service URL. Example: https://my-service-url", + "HTTP service URL is required": "HTTP service URL is required", "Deleting {{count}} resource sync_one": "Deleting {{count}} resource sync", "Deleting {{count}} resource sync_other": "Deleting {{count}} resource syncs", "{{count}} resource sync could not be deleted. Try deleting it manually._one": "{{count}} resource sync could not be deleted. Try deleting it manually.", @@ -988,9 +998,10 @@ "Delete repository": "Delete repository", "Private repository": "Private repository", "Public repository": "Public repository", - "Url": "Url", + "OCI registry": "OCI registry", "HTTP service": "HTTP service", "Git repository": "Git repository", + "Registry": "Registry", "Privacy": "Privacy", "Edge Manager will monitor the specified paths, import the defined fleets and synchronise devices": "Edge Manager will monitor the specified paths, import the defined fleets and synchronise devices", "Copy Url": "Copy Url", diff --git a/libs/types/index.ts b/libs/types/index.ts index 98cf07707..aceb1b4b3 100644 --- a/libs/types/index.ts +++ b/libs/types/index.ts @@ -15,6 +15,7 @@ export { ApplicationsSummaryStatusType } from './models/ApplicationsSummaryStatu export { ApplicationStatusType } from './models/ApplicationStatusType'; export type { ApplicationVolume } from './models/ApplicationVolume'; export type { ApplicationVolumeProviderSpec } from './models/ApplicationVolumeProviderSpec'; +export { ApplicationVolumeReclaimPolicy } from './models/ApplicationVolumeReclaimPolicy'; export type { ApplicationVolumeStatus } from './models/ApplicationVolumeStatus'; export { AppType } from './models/AppType'; export type { AuthConfig } from './models/AuthConfig'; @@ -77,6 +78,7 @@ export { DeviceUpdatedStatusType } from './models/DeviceUpdatedStatusType'; export type { DeviceUpdatePolicySpec } from './models/DeviceUpdatePolicySpec'; export type { DiskResourceMonitorSpec } from './models/DiskResourceMonitorSpec'; export type { DisruptionBudget } from './models/DisruptionBudget'; +export type { DockerAuth } from './models/DockerAuth'; export type { Duration } from './models/Duration'; export { EncodingType } from './models/EncodingType'; export type { EnrollmentConfig } from './models/EnrollmentConfig'; @@ -142,6 +144,9 @@ export type { OAuth2Introspection } from './models/OAuth2Introspection'; export type { OAuth2ProviderSpec } from './models/OAuth2ProviderSpec'; export type { ObjectMeta } from './models/ObjectMeta'; export type { ObjectReference } from './models/ObjectReference'; +export type { OciAuth } from './models/OciAuth'; +export { OciAuthType } from './models/OciAuthType'; +export { OciRepoSpec } from './models/OciRepoSpec'; export type { OIDCProviderSpec } from './models/OIDCProviderSpec'; export type { OpenShiftProviderSpec } from './models/OpenShiftProviderSpec'; export type { Organization } from './models/Organization'; diff --git a/libs/types/models/ApplicationVolume.ts b/libs/types/models/ApplicationVolume.ts index 50ea36136..13fafb995 100644 --- a/libs/types/models/ApplicationVolume.ts +++ b/libs/types/models/ApplicationVolume.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { ApplicationVolumeReclaimPolicy } from './ApplicationVolumeReclaimPolicy'; import type { ImageMountVolumeProviderSpec } from './ImageMountVolumeProviderSpec'; import type { ImageVolumeProviderSpec } from './ImageVolumeProviderSpec'; import type { MountVolumeProviderSpec } from './MountVolumeProviderSpec'; @@ -10,5 +11,6 @@ export type ApplicationVolume = ({ * Unique name of the volume used within the application. */ name: string; + reclaimPolicy?: ApplicationVolumeReclaimPolicy; } & (ImageVolumeProviderSpec | MountVolumeProviderSpec | ImageMountVolumeProviderSpec)); diff --git a/libs/types/models/ApplicationVolumeReclaimPolicy.ts b/libs/types/models/ApplicationVolumeReclaimPolicy.ts new file mode 100644 index 000000000..994c30de1 --- /dev/null +++ b/libs/types/models/ApplicationVolumeReclaimPolicy.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Defines how the agent handles a volume when the owning application is removed. + */ +export enum ApplicationVolumeReclaimPolicy { + RETAIN = 'Retain', +} diff --git a/libs/types/models/DockerAuth.ts b/libs/types/models/DockerAuth.ts new file mode 100644 index 000000000..8bf33e489 --- /dev/null +++ b/libs/types/models/DockerAuth.ts @@ -0,0 +1,19 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Docker-style authentication for OCI registries. + */ +export type DockerAuth = { + authType: 'docker'; + /** + * The username for registry authentication. + */ + username: string; + /** + * The password or token for registry authentication. + */ + password: string; +}; + diff --git a/libs/types/models/K8sProviderSpec.ts b/libs/types/models/K8sProviderSpec.ts index 20d3aa201..cad8009ef 100644 --- a/libs/types/models/K8sProviderSpec.ts +++ b/libs/types/models/K8sProviderSpec.ts @@ -30,5 +30,9 @@ export type K8sProviderSpec = { enabled?: boolean; organizationAssignment: AuthOrganizationAssignment; roleAssignment: AuthRoleAssignment; + /** + * Optional suffix to strip from ClusterRole names when normalizing role names. Used for multi-release deployments where ClusterRoles have namespace-specific names (e.g., flightctl-admin-). + */ + roleSuffix?: string; }; diff --git a/libs/types/models/OciAuth.ts b/libs/types/models/OciAuth.ts new file mode 100644 index 000000000..cb96da8a4 --- /dev/null +++ b/libs/types/models/OciAuth.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { DockerAuth } from './DockerAuth'; +/** + * Authentication for OCI registries. + */ +export type OciAuth = DockerAuth; + diff --git a/libs/types/models/OciAuthType.ts b/libs/types/models/OciAuthType.ts new file mode 100644 index 000000000..33d67b0f2 --- /dev/null +++ b/libs/types/models/OciAuthType.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * The type of authentication for OCI registries. + */ +export enum OciAuthType { + DOCKER = 'docker', +} diff --git a/libs/types/models/OciRepoSpec.ts b/libs/types/models/OciRepoSpec.ts new file mode 100644 index 000000000..7913cea3e --- /dev/null +++ b/libs/types/models/OciRepoSpec.ts @@ -0,0 +1,50 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { OciAuth } from './OciAuth'; +import type { RepoSpecType } from './RepoSpecType'; +/** + * OCI container registry specification. + */ +export type OciRepoSpec = { + /** + * The OCI registry hostname, FQDN, or IP address with optional port (e.g., quay.io, registry.redhat.io, myregistry.com:5000, 192.168.1.1:5000, [::1]:5000). + */ + registry: string; + /** + * URL scheme for connecting to the registry. + */ + scheme?: OciRepoSpec.scheme; + type: RepoSpecType; + /** + * Access mode for the registry: "Read" for read-only (pull), "ReadWrite" for read-write (pull and push). + */ + accessMode?: OciRepoSpec.accessMode; + ociAuth?: OciAuth; + /** + * Base64 encoded root CA. + */ + 'ca.crt'?: string; + /** + * Skip remote server verification. + */ + skipServerVerification?: boolean; +}; +export namespace OciRepoSpec { + /** + * URL scheme for connecting to the registry. + */ + export enum scheme { + HTTP = 'http', + HTTPS = 'https', + } + /** + * Access mode for the registry: "Read" for read-only (pull), "ReadWrite" for read-write (pull and push). + */ + export enum accessMode { + READ = 'Read', + READ_WRITE = 'ReadWrite', + } +} + diff --git a/libs/types/models/OpenShiftProviderSpec.ts b/libs/types/models/OpenShiftProviderSpec.ts index c901378c2..c98d13d8b 100644 --- a/libs/types/models/OpenShiftProviderSpec.ts +++ b/libs/types/models/OpenShiftProviderSpec.ts @@ -46,5 +46,13 @@ export type OpenShiftProviderSpec = { * The OpenShift cluster control plane URL. */ clusterControlPlaneUrl?: string; + /** + * If specified, only projects with this label will be considered. The label selector should be in the format 'key' or 'key=value'. If only the key is provided, any project with that label (regardless of value) will be included. This enables server-side filtering for better performance. + */ + projectLabelFilter?: string; + /** + * Optional suffix to strip from ClusterRole names when normalizing role names. Used for multi-release deployments where ClusterRoles have namespace-specific names (e.g., flightctl-admin-). + */ + roleSuffix?: string; }; diff --git a/libs/types/models/RepoSpecType.ts b/libs/types/models/RepoSpecType.ts index 60e3b1896..fa2e96a94 100644 --- a/libs/types/models/RepoSpecType.ts +++ b/libs/types/models/RepoSpecType.ts @@ -8,4 +8,5 @@ export enum RepoSpecType { GIT = 'git', HTTP = 'http', + OCI = 'oci', } diff --git a/libs/types/models/RepositorySpec.ts b/libs/types/models/RepositorySpec.ts index 7ad462a4c..c7e262898 100644 --- a/libs/types/models/RepositorySpec.ts +++ b/libs/types/models/RepositorySpec.ts @@ -4,9 +4,10 @@ /* eslint-disable */ import type { GenericRepoSpec } from './GenericRepoSpec'; import type { HttpRepoSpec } from './HttpRepoSpec'; +import type { OciRepoSpec } from './OciRepoSpec'; import type { SshRepoSpec } from './SshRepoSpec'; /** * RepositorySpec describes a configuration repository. */ -export type RepositorySpec = (GenericRepoSpec | HttpRepoSpec | SshRepoSpec); +export type RepositorySpec = (GenericRepoSpec | HttpRepoSpec | SshRepoSpec | OciRepoSpec); diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ConfigWithRepositoryTemplateForm.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ConfigWithRepositoryTemplateForm.tsx index 0a3fccf49..1bfda1a18 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ConfigWithRepositoryTemplateForm.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ConfigWithRepositoryTemplateForm.tsx @@ -5,13 +5,14 @@ import { PlusCircleIcon } from '@patternfly/react-icons/dist/js/icons/plus-circl import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/js/icons/exclamation-circle-icon'; import { TFunction, Trans } from 'react-i18next'; -import { RepoSpecType, Repository } from '@flightctl/types'; +import { GenericRepoSpec, HttpRepoSpec, RepoSpecType, Repository } from '@flightctl/types'; import { DeviceSpecConfigFormValues, GitConfigTemplate, HttpConfigTemplate } from '../../../../types/deviceSpec'; import { useTranslation } from '../../../../hooks/useTranslation'; import TextField from '../../../form/TextField'; import FormSelect from '../../../form/FormSelect'; import CreateRepositoryModal from '../../../modals/CreateRepositoryModal/CreateRepositoryModal'; import { FormGroupWithHelperText } from '../../../common/WithHelperText'; +import { getRepoUrlOrRegistry } from '../../../Repository/CreateRepository/utils'; type ConfigWithRepositoryTemplateFormProps = { repoType: RepoSpecType; @@ -30,9 +31,11 @@ const getRepositoryItems = ( ) => { const repositoryItems = repositories.reduce((acc, curr) => { if (curr.spec.type === repoType) { - acc[curr.metadata.name || ''] = { - label: curr.metadata.name, - description: curr.spec.url, + const description = getRepoUrlOrRegistry(curr.spec); + const repoName = curr.metadata.name || ''; + acc[repoName] = { + label: repoName, + description, }; } return acc; @@ -175,6 +178,7 @@ const ConfigWithRepositoryTemplateForm = ({ : getRepositoryItems(t, repositories, repoType, selectedRepoName); const selectedRepo = repositories.find((repo) => repo.metadata.name === selectedRepoName); + const repoSpec = selectedRepo?.spec as GenericRepoSpec | HttpRepoSpec | undefined; return ( <> @@ -209,7 +213,7 @@ const ConfigWithRepositoryTemplateForm = ({ )} diff --git a/libs/ui-components/src/components/Fleet/ImportFleetWizard/steps/RepositoryStep.tsx b/libs/ui-components/src/components/Fleet/ImportFleetWizard/steps/RepositoryStep.tsx index e1987838a..65cdf505a 100644 --- a/libs/ui-components/src/components/Fleet/ImportFleetWizard/steps/RepositoryStep.tsx +++ b/libs/ui-components/src/components/Fleet/ImportFleetWizard/steps/RepositoryStep.tsx @@ -3,7 +3,7 @@ import { FormGroup, FormSection, Grid, Radio } from '@patternfly/react-core'; import { FormikErrors, useFormikContext } from 'formik'; import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; -import { Repository } from '@flightctl/types'; +import { GenericRepoSpec, Repository } from '@flightctl/types'; import { ImportFleetFormValues } from '../types'; import { RepositoryForm } from '../../../Repository/CreateRepository/CreateRepositoryForm'; @@ -36,7 +36,7 @@ const ExistingRepoForm = ({ repositories }: { repositories: Repository[] }) => { const { values } = useFormikContext(); const currentRepo = repositories.find((r) => r.metadata.name === values.existingRepo); - + const repoSpec = currentRepo?.spec as GenericRepoSpec | undefined; // Only git repositories can be used for importing fleets; return ( <> @@ -60,7 +60,7 @@ const ExistingRepoForm = ({ repositories }: { repositories: Repository[] }) => { - {currentRepo.spec.url} + {repoSpec?.url || '-'} diff --git a/libs/ui-components/src/components/Repository/CreateRepository/CreateRepositoryForm.tsx b/libs/ui-components/src/components/Repository/CreateRepository/CreateRepositoryForm.tsx index 29b594224..94aeddf41 100644 --- a/libs/ui-components/src/components/Repository/CreateRepository/CreateRepositoryForm.tsx +++ b/libs/ui-components/src/components/Repository/CreateRepository/CreateRepositoryForm.tsx @@ -31,7 +31,7 @@ import { handlePromises, repositorySchema, } from './utils'; -import { RepoSpecType, Repository, ResourceSync } from '@flightctl/types'; +import { OciRepoSpec, RepoSpecType, Repository, ResourceSync } from '@flightctl/types'; import { getErrorMessage } from '../../../utils/error'; import LeaveFormConfirmation from '../../common/LeaveFormConfirmation'; import LabelWithHelperText, { FormGroupWithHelperText } from '../../common/WithHelperText'; @@ -52,6 +52,40 @@ const AdvancedSection = () => { const { t } = useTranslation(); const { values } = useFormikContext(); const showConfigTypeRadios = values.repoType === RepoSpecType.GIT; + const isOciRepo = values.repoType === RepoSpecType.OCI; + + if (isOciRepo) { + return ( + + + + + + + + + + + } + /> + + + + + + + + + ); + } return ( @@ -142,7 +176,7 @@ const AdvancedSection = () => { const RepositoryType = ({ isEdit }: { isEdit?: boolean }) => { const { t } = useTranslation(); const { values, setFieldValue, validateForm } = useFormikContext(); - const [showConfirmChangeType, setShowConfirmChangeType] = React.useState(); + const [showConfirmChangeType, setShowConfirmChangeType] = React.useState(); if (!values.showRepoTypes) { return null; @@ -150,26 +184,41 @@ const RepositoryType = ({ isEdit }: { isEdit?: boolean }) => { const isRepoTypeChangeDisabled = values.allowedRepoTypes?.length === 1; - const doChangeRepoType = (toType?: RepoSpecType) => { - if (!toType) { - toType = values.repoType === RepoSpecType.GIT ? RepoSpecType.HTTP : RepoSpecType.GIT; + const doChangeRepoType = (toType: RepoSpecType) => { + if (toType === RepoSpecType.GIT) { + void setFieldValue('repoType', RepoSpecType.GIT); + void setFieldValue('httpConfig.token', undefined); + void setFieldValue('canUseResourceSyncs', true); } + if (toType === RepoSpecType.HTTP) { void setFieldValue('repoType', RepoSpecType.HTTP); void setFieldValue('configType', 'http'); void setFieldValue('useResourceSyncs', false); - } else { - void setFieldValue('repoType', RepoSpecType.GIT); - void setFieldValue('httpConfig.token', undefined); + void setFieldValue('canUseResourceSyncs', false); + } + + if (toType === RepoSpecType.OCI) { + void setFieldValue('repoType', RepoSpecType.OCI); + void setFieldValue('useResourceSyncs', false); + void setFieldValue('canUseResourceSyncs', false); + if (!values.ociConfig) { + void setFieldValue('ociConfig', { + registry: '', + scheme: OciRepoSpec.scheme.HTTPS, + accessMode: OciRepoSpec.accessMode.READ, + }); + } } void validateForm(); }; - const onRepoTypeChange = (repoType: unknown) => { + const onRepoTypeChange = (type: unknown) => { + const repoType = type as RepoSpecType; if (isEdit) { - setShowConfirmChangeType(true); + setShowConfirmChangeType(repoType); } else { - doChangeRepoType(repoType as RepoSpecType); + doChangeRepoType(repoType); } }; @@ -198,6 +247,17 @@ const RepositoryType = ({ isEdit }: { isEdit?: boolean }) => { isDisabled={isRepoTypeChangeDisabled} /> + + + {showConfirmChangeType && ( @@ -211,8 +271,8 @@ const RepositoryType = ({ isEdit }: { isEdit?: boolean }) => { key="change" variant={ButtonVariant.primary} onClick={() => { - setShowConfirmChangeType(false); - doChangeRepoType(); + setShowConfirmChangeType(undefined); + doChangeRepoType(showConfirmChangeType); }} > {t('Change')} @@ -221,7 +281,7 @@ const RepositoryType = ({ isEdit }: { isEdit?: boolean }) => { key="cancel" variant="link" onClick={() => { - setShowConfirmChangeType(false); + setShowConfirmChangeType(undefined); }} > {t('Cancel')} @@ -235,6 +295,8 @@ const RepositoryType = ({ isEdit }: { isEdit?: boolean }) => { export const RepositoryForm = ({ isEdit }: { isEdit?: boolean }) => { const { t } = useTranslation(); + const { values } = useFormikContext(); + const isOciRepo = values.repoType === RepoSpecType.OCI; return ( <> @@ -246,15 +308,69 @@ export const RepositoryForm = ({ isEdit }: { isEdit?: boolean }) => { resourceType="repositories" validations={getDnsSubdomainValidations(t)} /> - - - + {isOciRepo ? ( + + + + ) : ( + + + + )} + {isOciRepo && ( + + + + + + + + + + + + + + + + + + + + + + + )} } /> ); @@ -348,10 +464,10 @@ const CreateRepositoryForm: React.FC = ({ onSubmit={async (values) => { setErrors(undefined); if (repository) { - const patches = getRepositoryPatches(values, repository); + const specPatches = getRepositoryPatches(values, repository); try { - if (patches.length) { - await patch(`repositories/${repository.metadata.name}`, patches); + if (specPatches.length) { + await patch(`repositories/${repository.metadata.name}`, specPatches); } if (values.useResourceSyncs) { const storedRSs = resourceSyncs || []; diff --git a/libs/ui-components/src/components/Repository/CreateRepository/types.ts b/libs/ui-components/src/components/Repository/CreateRepository/types.ts index dd1fa25d0..1531fcd4b 100644 --- a/libs/ui-components/src/components/Repository/CreateRepository/types.ts +++ b/libs/ui-components/src/components/Repository/CreateRepository/types.ts @@ -1,4 +1,4 @@ -import { RepoSpecType } from '@flightctl/types'; +import { OciRepoSpec, RepoSpecType } from '@flightctl/types'; export type ResourceSyncFormValue = { name: string; @@ -37,6 +37,18 @@ export type RepositoryFormValues = { privateKeyPassphrase?: string; skipServerVerification?: boolean; }; + ociConfig?: { + registry: string; + scheme?: OciRepoSpec.scheme; + accessMode?: OciRepoSpec.accessMode; + caCrt?: string; + ociAuth?: { + use?: boolean; + username?: string; + password?: string; + }; + skipServerVerification?: boolean; + }; canUseResourceSyncs: boolean; useResourceSyncs: boolean; resourceSyncs: ResourceSyncFormValue[]; diff --git a/libs/ui-components/src/components/Repository/CreateRepository/utils.ts b/libs/ui-components/src/components/Repository/CreateRepository/utils.ts index d92d4806c..ad27af5f0 100644 --- a/libs/ui-components/src/components/Repository/CreateRepository/utils.ts +++ b/libs/ui-components/src/components/Repository/CreateRepository/utils.ts @@ -1,8 +1,11 @@ import * as Yup from 'yup'; import { TFunction } from 'i18next'; import { + DockerAuth, HttpConfig, HttpRepoSpec, + OciAuthType, + OciRepoSpec, PatchRequest, RepoSpecType, Repository, @@ -29,6 +32,14 @@ const jwtTokenRegexp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; export const isHttpRepoSpec = (repoSpec: RepositorySpec): repoSpec is HttpRepoSpec => !!(repoSpec['httpConfig'] || (repoSpec as HttpRepoSpec).validationSuffix); export const isSshRepoSpec = (repoSpec: RepositorySpec): repoSpec is SshRepoSpec => !!repoSpec['sshConfig']; +export const isOciRepoSpec = (repoSpec: RepositorySpec): repoSpec is OciRepoSpec => repoSpec.type === RepoSpecType.OCI; + +export const getRepoUrlOrRegistry = (repoSpec: RepositorySpec): string => { + if (isOciRepoSpec(repoSpec)) { + return repoSpec.registry || ''; + } + return repoSpec.url || ''; +}; export const getInitValues = ({ repository, @@ -43,12 +54,13 @@ export const getInitValues = ({ showRepoTypes?: boolean; }; }): RepositoryFormValues => { - const useRSs = options?.canUseResourceSyncs ?? true; + const configAllowsResourceSyncs = options?.canUseResourceSyncs ?? true; if (!repository) { const selectedRepoType = options?.allowedRepoTypes?.length === 1 ? options.allowedRepoTypes[0] : RepoSpecType.GIT; + const canUseRSs = selectedRepoType === RepoSpecType.GIT && configAllowsResourceSyncs; - return { + const initValues: RepositoryFormValues = { exists: false, repoType: selectedRepoType, allowedRepoTypes: options?.allowedRepoTypes, @@ -57,9 +69,8 @@ export const getInitValues = ({ url: '', useAdvancedConfig: false, configType: 'http', - - canUseResourceSyncs: useRSs, - useResourceSyncs: useRSs, + canUseResourceSyncs: canUseRSs, + useResourceSyncs: canUseRSs, resourceSyncs: [ { name: '', @@ -68,20 +79,32 @@ export const getInitValues = ({ }, ], }; + + if (selectedRepoType === RepoSpecType.OCI) { + initValues.ociConfig = { + registry: '', + scheme: OciRepoSpec.scheme.HTTPS, + accessMode: OciRepoSpec.accessMode.READ, + }; + } + + return initValues; } + const canUseRSs = repository.spec.type === RepoSpecType.GIT && configAllowsResourceSyncs; + const formValues: RepositoryFormValues = { exists: true, name: repository.metadata.name || '', - url: repository.spec.url || '', + url: isOciRepoSpec(repository.spec) ? '' : repository.spec.url || '', repoType: repository.spec.type, validationSuffix: 'validationSuffix' in repository.spec ? repository.spec.validationSuffix : '', allowedRepoTypes: options?.allowedRepoTypes, showRepoTypes: options?.showRepoTypes ?? true, - useResourceSyncs: !!resourceSyncs?.length, useAdvancedConfig: false, configType: 'http', - canUseResourceSyncs: useRSs, + canUseResourceSyncs: canUseRSs, + useResourceSyncs: canUseRSs && !!resourceSyncs?.length, resourceSyncs: resourceSyncs?.length ? resourceSyncs .filter((rs) => rs.spec.repository === repository.metadata.name) @@ -94,7 +117,29 @@ export const getInitValues = ({ : [{ name: '', path: '', targetRevision: '' }], }; - if (isHttpRepoSpec(repository.spec)) { + if (isOciRepoSpec(repository.spec)) { + formValues.useAdvancedConfig = !!( + repository.spec.ociAuth || + repository.spec['ca.crt'] || + repository.spec.skipServerVerification || + repository.spec.scheme || + repository.spec.accessMode + ); + formValues.ociConfig = { + registry: repository.spec.registry, + scheme: repository.spec.scheme || OciRepoSpec.scheme.HTTPS, + accessMode: repository.spec.accessMode || OciRepoSpec.accessMode.READ, + ociAuth: repository.spec.ociAuth + ? { + use: true, + username: repository.spec.ociAuth.username, + password: repository.spec.ociAuth.password, + } + : undefined, + caCrt: repository.spec['ca.crt'] ? atob(repository.spec['ca.crt']) : undefined, + skipServerVerification: repository.spec.skipServerVerification, + }; + } else if (isHttpRepoSpec(repository.spec)) { formValues.useAdvancedConfig = true; formValues.configType = 'http'; formValues.httpConfig = { @@ -126,19 +171,112 @@ export const getInitValues = ({ }; export const getRepositoryPatches = (values: RepositoryFormValues, repository: Repository): PatchRequest => { + // If repoType changes, replace the entire spec + if (values.repoType !== repository.spec.type) { + const newRepository = getRepository(values); + return [ + { + op: 'replace', + path: '/spec', + value: newRepository.spec, + }, + ]; + } + const patches: PatchRequest = []; - appendJSONPatch({ - patches, - newValue: values.repoType, - originalValue: repository.spec.type, - path: '/spec/type', - }); - appendJSONPatch({ - patches, - newValue: values.url, - originalValue: repository.spec.url, - path: '/spec/url', - }); + + // Handle OCI repository patches first + if (values.repoType === RepoSpecType.OCI) { + const ociRepoSpec = repository.spec as OciRepoSpec; + if (values.ociConfig) { + appendJSONPatch({ + patches, + newValue: values.ociConfig.registry, + originalValue: ociRepoSpec.registry, + path: '/spec/registry', + }); + + if (!values.useAdvancedConfig) { + if (ociRepoSpec.ociAuth) { + patches.push({ op: 'remove', path: '/spec/ociAuth' }); + } + if (ociRepoSpec['ca.crt']) { + patches.push({ op: 'remove', path: '/spec/ca.crt' }); + } + if (ociRepoSpec.skipServerVerification !== undefined) { + patches.push({ op: 'remove', path: '/spec/skipServerVerification' }); + } + if (ociRepoSpec.scheme) { + patches.push({ op: 'remove', path: '/spec/scheme' }); + } + if (ociRepoSpec.accessMode) { + patches.push({ op: 'remove', path: '/spec/accessMode' }); + } + return patches; + } + + appendJSONPatch({ + patches, + newValue: values.ociConfig.scheme || OciRepoSpec.scheme.HTTPS, + originalValue: ociRepoSpec.scheme, + path: '/spec/scheme', + }); + + appendJSONPatch({ + patches, + newValue: values.ociConfig.accessMode || OciRepoSpec.accessMode.READ, + originalValue: ociRepoSpec.accessMode, + path: '/spec/accessMode', + }); + + appendJSONPatch({ + patches, + newValue: values.ociConfig.skipServerVerification, + originalValue: ociRepoSpec.skipServerVerification, + path: '/spec/skipServerVerification', + }); + + if (values.ociConfig.skipServerVerification && ociRepoSpec['ca.crt']) { + patches.push({ op: 'remove', path: '/spec/ca.crt' }); + } else { + const caCrt = values.ociConfig.caCrt; + appendJSONPatch({ + patches, + newValue: caCrt ? btoa(caCrt) : caCrt, + originalValue: ociRepoSpec['ca.crt'], + path: '/spec/ca.crt', + }); + } + + const ociAuth = values.ociConfig.ociAuth; + if (ociAuth?.use && ociAuth.username && ociAuth.password) { + const ociAuthValue: DockerAuth = { + authType: OciAuthType.DOCKER, + username: ociAuth.username, + password: ociAuth.password, + }; + appendJSONPatch({ + patches, + newValue: ociAuthValue, + originalValue: ociRepoSpec.ociAuth, + path: '/spec/ociAuth', + }); + } else if (ociRepoSpec.ociAuth) { + patches.push({ op: 'remove', path: '/spec/ociAuth' }); + } + } + return patches; + } + + // Handle Git/Http repository patches + if ('url' in repository.spec) { + appendJSONPatch({ + patches, + newValue: values.url, + originalValue: repository.spec.url, + path: '/spec/url', + }); + } if (!values.useAdvancedConfig) { if (isHttpRepoSpec(repository.spec)) { @@ -429,26 +567,14 @@ export const singleResourceSyncSchema = (t: TFunction, existingRSs: ResourceSync }); }; +// Regex for registry hostname: FQDN, IP address (IPv4 or IPv6), with optional port, matching as much as possible of the backend pattern +const registryHostnameRegex = + /^(([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)*[a-z]([-a-z0-9]*[a-z0-9])?|[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|\[[a-fA-F0-9:]+\])(:[0-9]{1,5})?$/; + export const repositorySchema = (t: TFunction, repository: Repository | undefined) => (values: RepositoryFormValues) => { - return Yup.object({ + const baseSchema = { name: validKubernetesDnsSubdomain(t, { isRequired: !repository }), - url: Yup.string().when('repoType', { - is: (repoType: RepoSpecType) => repoType === RepoSpecType.GIT, - then: () => - Yup.string() - .matches( - gitRepoUrlRegex, - t('Enter a valid repository URL. Example: {{ demoRepositoryUrl }}', { - demoRepositoryUrl: 'https://github.com/flightctl/flightctl-demos', - }), - ) - .defined(t('Repository URL is required')), - otherwise: () => - Yup.string() - .matches(httpRepoUrlRegex, t('Enter a valid HTTP service URL. Example: https://my-service-url')) - .defined(t('HTTP service URL is required')), - }), configType: values.useAdvancedConfig ? Yup.string().required(t('Repository type is required')) : Yup.string(), httpConfig: Yup.object({ basicAuth: Yup.object({ @@ -467,10 +593,89 @@ export const repositorySchema = }), useResourceSyncs: Yup.boolean(), resourceSyncs: values.useResourceSyncs ? repoSyncSchema(t, values.resourceSyncs) : Yup.array(), + }; + + if (values.repoType === RepoSpecType.OCI) { + return Yup.object({ + ...baseSchema, + url: Yup.string(), + ociConfig: Yup.object({ + registry: Yup.string() + .matches( + registryHostnameRegex, + t('Enter a valid registry hostname (e.g., quay.io, registry.redhat.io, myregistry.com:5000)'), + ) + .required(t('Registry hostname is required')), + scheme: Yup.string().oneOf([OciRepoSpec.scheme.HTTP, OciRepoSpec.scheme.HTTPS]), + accessMode: Yup.string().oneOf([OciRepoSpec.accessMode.READ, OciRepoSpec.accessMode.READ_WRITE]), + ociAuth: Yup.object({ + use: Yup.boolean(), + username: values.ociConfig?.ociAuth?.use ? Yup.string().required(t('Username is required')) : Yup.string(), + password: values.ociConfig?.ociAuth?.use ? Yup.string().required(t('Password is required')) : Yup.string(), + }), + caCrt: Yup.string(), + skipServerVerification: Yup.boolean(), + }), + }); + } + + return Yup.object({ + ...baseSchema, + url: Yup.string().when('repoType', { + is: (repoType: RepoSpecType) => repoType === RepoSpecType.GIT, + then: () => + Yup.string() + .matches( + gitRepoUrlRegex, + t('Enter a valid repository URL. Example: {{ demoRepositoryUrl }}', { + demoRepositoryUrl: 'https://github.com/flightctl/flightctl-demos', + }), + ) + .defined(t('Repository URL is required')), + otherwise: () => + Yup.string() + .matches(httpRepoUrlRegex, t('Enter a valid HTTP service URL. Example: https://my-service-url')) + .defined(t('HTTP service URL is required')), + }), + ociConfig: Yup.object(), }); }; export const getRepository = (values: Omit): Repository => { + if (values.repoType === RepoSpecType.OCI && values.ociConfig) { + const ociRepoSpec: OciRepoSpec = { + registry: values.ociConfig.registry, + type: RepoSpecType.OCI, + scheme: values.ociConfig.scheme || OciRepoSpec.scheme.HTTPS, + accessMode: values.ociConfig.accessMode || OciRepoSpec.accessMode.READ, + }; + + if (values.ociConfig.skipServerVerification) { + ociRepoSpec.skipServerVerification = true; + } + + if (values.ociConfig.caCrt && !values.ociConfig.skipServerVerification) { + ociRepoSpec['ca.crt'] = btoa(values.ociConfig.caCrt); + } + + if (values.ociConfig.ociAuth?.use && values.ociConfig.ociAuth.username && values.ociConfig.ociAuth.password) { + ociRepoSpec.ociAuth = { + authType: OciAuthType.DOCKER, + username: values.ociConfig.ociAuth.username, + password: values.ociConfig.ociAuth.password, + }; + } + + return { + apiVersion: API_VERSION, + kind: 'Repository', + metadata: { + name: values.name, + }, + spec: ociRepoSpec, + }; + } + const spec: RepositorySpec = { url: values.url, type: values.repoType, diff --git a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx index 86db08a8c..ca0e1ecc0 100644 --- a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails.tsx @@ -93,7 +93,7 @@ const RepositoryDetails = () => { - {canListRS && repoDetails.spec.type !== RepoSpecType.HTTP && ( + {canListRS && repoDetails.spec.type === RepoSpecType.GIT && ( diff --git a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx index ccf33a8b0..46cabac2a 100644 --- a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx @@ -11,13 +11,13 @@ import { import { LockIcon } from '@patternfly/react-icons/dist/js/icons/lock-icon'; import { LockOpenIcon } from '@patternfly/react-icons/dist/js/icons/lock-open-icon'; -import { RepoSpecType, Repository } from '@flightctl/types'; +import { Repository } from '@flightctl/types'; import { getLastTransitionTimeText, getRepositorySyncStatus } from '../../../utils/status/repository'; import { useTranslation } from '../../../hooks/useTranslation'; import FlightControlDescriptionList from '../../common/FlightCtlDescriptionList'; import RepositoryStatus from '../../Status/RepositoryStatus'; -import { isHttpRepoSpec, isSshRepoSpec } from '../CreateRepository/utils'; +import { getRepoUrlOrRegistry, isHttpRepoSpec, isOciRepoSpec, isSshRepoSpec } from '../CreateRepository/utils'; import { GitRepositoryLink, HttpRepositoryUrl } from './RepositorySource'; const RepoPrivacy = ({ repo }: { repo: Repository }) => { @@ -31,6 +31,10 @@ const RepoPrivacy = ({ repo }: { repo: Repository }) => { if (repo.spec.sshConfig.sshPrivateKey) { isPrivate = true; } + } else if (isOciRepoSpec(repo.spec)) { + if (repo.spec.ociAuth) { + isPrivate = true; + } } return isPrivate ? ( @@ -50,28 +54,43 @@ const RepoPrivacy = ({ repo }: { repo: Repository }) => { ); }; +const RegistryOrUrl = ({ repo }: { repo: Repository }) => { + const urlOrRegistry = getRepoUrlOrRegistry(repo.spec); + if (isOciRepoSpec(repo.spec)) { + return
{urlOrRegistry}
; + } + if (isHttpRepoSpec(repo.spec)) { + return ; + } + return ; +}; + const DetailsTab = ({ repoDetails }: { repoDetails: Repository }) => { const { t } = useTranslation(); + + let repoLabel = ''; + if (isOciRepoSpec(repoDetails.spec)) { + repoLabel = t('OCI registry'); + } else if (isHttpRepoSpec(repoDetails.spec)) { + repoLabel = t('HTTP service'); + } else { + repoLabel = t('Git repository'); + } + return ( {t('Details')} - {t('Url')} + {isOciRepoSpec(repoDetails.spec) ? t('Registry') : t('URL')} - {repoDetails?.spec.type === RepoSpecType.HTTP ? ( - - ) : ( - - )} + {t('Type')} - - {repoDetails?.spec.type === RepoSpecType.HTTP ? t('HTTP service') : t('Git repository')} - + {repoLabel} diff --git a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositorySourceList.tsx b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositorySourceList.tsx index e57cb0209..92636df6e 100644 --- a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositorySourceList.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositorySourceList.tsx @@ -7,6 +7,7 @@ import { ConfigSourceProvider, getRepoName, isRepoConfig } from '../../../types/ import { isPromiseRejected } from '../../../types/typeUtils'; import { getErrorMessage } from '../../../utils/error'; import { getConfigDetails } from './RepositorySource'; +import { getRepoUrlOrRegistry } from '../CreateRepository/utils'; const useArrayEq = (array: string[]) => { const prevArrayRef = React.useRef(array); @@ -45,7 +46,7 @@ const RepositorySourceList = ({ configs }: { configs: Array { const { t } = useTranslation(); @@ -75,7 +76,7 @@ const getColumns = (t: TFunction): TableColumn[] => [ name: t('Type'), }, { - name: t('Url'), + name: t('URL'), }, { name: t('Sync status'), @@ -133,9 +134,15 @@ const RepositoryTableRow = ({ - {repository.spec.type === RepoSpecType.HTTP ? t('HTTP service') : t('Git repository')} + {repository.spec.type === RepoSpecType.OCI + ? t('OCI registry') + : repository.spec.type === RepoSpecType.HTTP + ? t('HTTP service') + : t('Git repository')} + + + {getRepoUrlOrRegistry(repository.spec) || '-'} - {repository.spec.url || '-'} diff --git a/libs/ui-components/src/components/form/FormSelect.tsx b/libs/ui-components/src/components/form/FormSelect.tsx index 359aa3b00..061a616d6 100644 --- a/libs/ui-components/src/components/form/FormSelect.tsx +++ b/libs/ui-components/src/components/form/FormSelect.tsx @@ -6,7 +6,7 @@ import ErrorHelperText, { DefaultHelperText } from './FieldHelperText'; import './FormSelect.css'; -type SelectItem = { label: string; description?: string }; +type SelectItem = { label: string; description?: string | React.ReactNode }; type FormSelectProps = { name: string; From ff3f4a75105684a36e1f021b4ab209e817ddc29f Mon Sep 17 00:00:00 2001 From: Celia Amador Date: Fri, 9 Jan 2026 11:25:06 +0100 Subject: [PATCH 2/2] Reusable repoLabel function --- libs/i18n/locales/en/translation.json | 6 +++--- .../Repository/CreateRepository/utils.ts | 11 +++++++++++ .../RepositoryGeneralDetailsCard.tsx | 17 ++++++++--------- .../components/Repository/RepositoryList.tsx | 10 ++-------- .../src/components/form/FormSelect.tsx | 2 +- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index d60f6b929..d5dc17032 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -969,6 +969,9 @@ "For example: {{exampleFile}}": "For example: {{exampleFile}}", "Remove resource sync": "Remove resource sync", "Add another resource sync": "Add another resource sync", + "HTTP service": "HTTP service", + "OCI registry": "OCI registry", + "Git repository": "Git repository", "Target revision is required.": "Target revision is required.", "Must be an absolute path.": "Must be an absolute path.", "Repository type is required": "Repository type is required", @@ -998,9 +1001,6 @@ "Delete repository": "Delete repository", "Private repository": "Private repository", "Public repository": "Public repository", - "OCI registry": "OCI registry", - "HTTP service": "HTTP service", - "Git repository": "Git repository", "Registry": "Registry", "Privacy": "Privacy", "Edge Manager will monitor the specified paths, import the defined fleets and synchronise devices": "Edge Manager will monitor the specified paths, import the defined fleets and synchronise devices", diff --git a/libs/ui-components/src/components/Repository/CreateRepository/utils.ts b/libs/ui-components/src/components/Repository/CreateRepository/utils.ts index ad27af5f0..7b03b5770 100644 --- a/libs/ui-components/src/components/Repository/CreateRepository/utils.ts +++ b/libs/ui-components/src/components/Repository/CreateRepository/utils.ts @@ -41,6 +41,17 @@ export const getRepoUrlOrRegistry = (repoSpec: RepositorySpec): string => { return repoSpec.url || ''; }; +export const getRepoTypeLabel = (t: TFunction, repoType: RepoSpecType): string => { + switch (repoType) { + case RepoSpecType.HTTP: + return t('HTTP service'); + case RepoSpecType.OCI: + return t('OCI registry'); + default: + return t('Git repository'); + } +}; + export const getInitValues = ({ repository, resourceSyncs, diff --git a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx index 46cabac2a..e18b620e8 100644 --- a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx @@ -17,7 +17,13 @@ import { getLastTransitionTimeText, getRepositorySyncStatus } from '../../../uti import { useTranslation } from '../../../hooks/useTranslation'; import FlightControlDescriptionList from '../../common/FlightCtlDescriptionList'; import RepositoryStatus from '../../Status/RepositoryStatus'; -import { getRepoUrlOrRegistry, isHttpRepoSpec, isOciRepoSpec, isSshRepoSpec } from '../CreateRepository/utils'; +import { + getRepoTypeLabel, + getRepoUrlOrRegistry, + isHttpRepoSpec, + isOciRepoSpec, + isSshRepoSpec, +} from '../CreateRepository/utils'; import { GitRepositoryLink, HttpRepositoryUrl } from './RepositorySource'; const RepoPrivacy = ({ repo }: { repo: Repository }) => { @@ -68,14 +74,7 @@ const RegistryOrUrl = ({ repo }: { repo: Repository }) => { const DetailsTab = ({ repoDetails }: { repoDetails: Repository }) => { const { t } = useTranslation(); - let repoLabel = ''; - if (isOciRepoSpec(repoDetails.spec)) { - repoLabel = t('OCI registry'); - } else if (isHttpRepoSpec(repoDetails.spec)) { - repoLabel = t('HTTP service'); - } else { - repoLabel = t('Git repository'); - } + const repoLabel = getRepoTypeLabel(t, repoDetails.spec.type); return ( diff --git a/libs/ui-components/src/components/Repository/RepositoryList.tsx b/libs/ui-components/src/components/Repository/RepositoryList.tsx index e1bfaa1ff..b88a4e207 100644 --- a/libs/ui-components/src/components/Repository/RepositoryList.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryList.tsx @@ -33,7 +33,7 @@ import { RESOURCE, VERB } from '../../types/rbac'; import { usePermissionsContext } from '../common/PermissionsContext'; import { useRepositories } from './useRepositories'; import TablePagination from '../Table/TablePagination'; -import { getRepoUrlOrRegistry } from './CreateRepository/utils'; +import { getRepoTypeLabel, getRepoUrlOrRegistry } from './CreateRepository/utils'; const CreateRepositoryButton = ({ buttonText }: { buttonText?: string }) => { const { t } = useTranslation(); @@ -133,13 +133,7 @@ const RepositoryTableRow = ({ - - {repository.spec.type === RepoSpecType.OCI - ? t('OCI registry') - : repository.spec.type === RepoSpecType.HTTP - ? t('HTTP service') - : t('Git repository')} - + {getRepoTypeLabel(t, repository.spec.type)} {getRepoUrlOrRegistry(repository.spec) || '-'} diff --git a/libs/ui-components/src/components/form/FormSelect.tsx b/libs/ui-components/src/components/form/FormSelect.tsx index 061a616d6..53a43eb3b 100644 --- a/libs/ui-components/src/components/form/FormSelect.tsx +++ b/libs/ui-components/src/components/form/FormSelect.tsx @@ -6,7 +6,7 @@ import ErrorHelperText, { DefaultHelperText } from './FieldHelperText'; import './FormSelect.css'; -type SelectItem = { label: string; description?: string | React.ReactNode }; +type SelectItem = { label: string; description?: React.ReactNode }; type FormSelectProps = { name: string;