diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-capability.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-capability.tsx new file mode 100644 index 00000000000..5387feea0a7 --- /dev/null +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-capability.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { useCurrentChannel } from '../../hooks/useCurrentChannel'; +import { PackageManifestKind } from '../../types'; +import { CapabilityLevel } from './operator-hub-item-details'; + +export const OperatorCapability: React.FC = ({ packageManifest }) => { + const currentChannel = useCurrentChannel(packageManifest); + const capabilities = currentChannel?.currentCSVDesc?.annotations?.capabilities; + + return capabilities ? : <>-; +}; + +type OperatorCapabilityProps = { + packageManifest: PackageManifestKind; +}; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-container-image.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-container-image.tsx new file mode 100644 index 00000000000..fc07f46f994 --- /dev/null +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-container-image.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { useCurrentChannel } from '../../hooks/useCurrentChannel'; +import { PackageManifestKind } from '../../types'; + +export const OperatorContainerImage: React.FC = ({ + packageManifest, +}) => { + const currentChannel = useCurrentChannel(packageManifest); + const containerImage = currentChannel?.currentCSVDesc?.annotations?.containerImage; + + return <>{containerImage || '-'}; +}; + +type OperatorContainerImageProps = { + packageManifest: PackageManifestKind; +}; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-created-at.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-created-at.tsx new file mode 100644 index 00000000000..f70fa626c57 --- /dev/null +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-created-at.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { Timestamp } from '@console/shared/src/components/datetime/Timestamp'; +import { useCurrentChannel } from '../../hooks/useCurrentChannel'; +import { PackageManifestKind } from '../../types'; + +export const OperatorCreatedAt: React.FC = ({ packageManifest }) => { + const currentChannel = useCurrentChannel(packageManifest); + const createdAt = currentChannel?.currentCSVDesc?.annotations?.createdAt; + + return createdAt && !Number.isNaN(Date.parse(createdAt)) ? ( + + ) : ( + <>{createdAt || '-'} + ); +}; + +type OperatorCreatedAtProps = { + packageManifest: PackageManifestKind; +}; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-item-details.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-item-details.tsx index fe43679d360..2c4e5aa6bf9 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-item-details.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-item-details.tsx @@ -34,7 +34,12 @@ import { DeprecatedOperatorWarningAlert } from '../deprecated-operator-warnings/ import { useDeprecatedOperatorWarnings } from '../deprecated-operator-warnings/use-deprecated-operator-warnings'; import { defaultChannelNameFor } from '../index'; import { OperatorChannelSelect, OperatorVersionSelect } from './operator-channel-version-select'; -import { isAWSSTSCluster, isAzureWIFCluster, isGCPWIFCluster } from './operator-hub-utils'; +import { + isAWSSTSCluster, + isAzureWIFCluster, + isGCPWIFCluster, + getInfrastructureFeatures, +} from './operator-hub-utils'; import { InfrastructureFeature, OperatorHubItem } from './index'; // t('olm~Basic Install'), @@ -225,7 +230,6 @@ const OperatorHubItemDetailsHint: React.FCC = ( export const OperatorDescription: React.FCC = ({ catalogSource, description, - infraFeatures, installed, isInstalling, subscription, @@ -251,6 +255,24 @@ export const OperatorDescription: React.FCC = ({ const currentCSVDescription = useCurrentCSVDescription(packageManifest); const selectedChannelDescription = currentCSVDescription?.description || longDescription; const packageManifestStatus = packageManifest?.status; + + // Get infrastructure features from the current channel's CSV description + const infraFeatures = useMemo(() => { + const currentCSVAnnotations = currentCSVDescription?.annotations ?? {}; + return getInfrastructureFeatures(currentCSVAnnotations, { + clusterIsAWSSTS, + clusterIsAzureWIF, + clusterIsGCPWIF, + onError: (error) => { + // eslint-disable-next-line no-console + console.warn( + `Error parsing infrastructure features from PackageManifest "${packageManifest?.metadata?.name}":`, + error, + ); + }, + }); + }, [currentCSVDescription, clusterIsAWSSTS, clusterIsAzureWIF, clusterIsGCPWIF, packageManifest]); + const [isTokenAuth, isTokenAuthGCP] = useMemo(() => { return [ (infraFeatures ?? []).includes(InfrastructureFeature.TokenAuth), @@ -337,7 +359,6 @@ export const OperatorHubItemDetails: React.FCC = ({ catalogSource, source, description, - infraFeatures, installed, isInstalling, longDescription, @@ -363,6 +384,28 @@ export const OperatorHubItemDetails: React.FCC = ({ const mappedData = (data) => data?.map?.((d) =>
{d}
) ?? notAvailable; + const selectedUpdateChannel = updateChannel || defaultChannelNameFor(obj); + const clusterIsAWSSTS = isAWSSTSCluster(cloudCredentials, infrastructure, authentication); + const clusterIsAzureWIF = isAzureWIFCluster(cloudCredentials, infrastructure, authentication); + const clusterIsGCPWIF = isGCPWIFCluster(cloudCredentials, infrastructure, authentication); + + // Get infrastructure features from the current channel's CSV description + const infraFeatures = useMemo(() => { + const currentCSVAnnotations = currentCSVDescription?.annotations ?? {}; + return getInfrastructureFeatures(currentCSVAnnotations, { + clusterIsAWSSTS, + clusterIsAzureWIF, + clusterIsGCPWIF, + onError: (error) => { + // eslint-disable-next-line no-console + console.warn( + `Error parsing infrastructure features from PackageManifest "${obj?.metadata?.name}":`, + error, + ); + }, + }); + }, [currentCSVDescription, clusterIsAWSSTS, clusterIsAzureWIF, clusterIsGCPWIF, obj]); + const mappedInfraFeatures = mappedData(infraFeatures); const mappedValidSubscription = mappedData(validSubscription); @@ -380,11 +423,6 @@ export const OperatorHubItemDetails: React.FCC = ({ return null; }, [marketplaceSupportWorkflow]); - const selectedUpdateChannel = updateChannel || defaultChannelNameFor(obj); - const clusterIsAWSSTS = isAWSSTSCluster(cloudCredentials, infrastructure, authentication); - const clusterIsAzureWIF = isAzureWIFCluster(cloudCredentials, infrastructure, authentication); - const clusterIsGCPWIF = isGCPWIFCluster(cloudCredentials, infrastructure, authentication); - return item ? (
@@ -453,7 +491,6 @@ export const OperatorHubItemDetails: React.FCC = ({ = ({ + packageManifest, + clusterIsAWSSTS, + clusterIsAzureWIF, + clusterIsGCPWIF, +}) => { + const currentChannel = useCurrentChannel(packageManifest); + const currentCSVAnnotations: CSVAnnotations = currentChannel?.currentCSVDesc?.annotations ?? {}; + + const infrastructureFeatures = getInfrastructureFeatures(currentCSVAnnotations, { + clusterIsAWSSTS, + clusterIsAzureWIF, + clusterIsGCPWIF, + onError: (error) => + // eslint-disable-next-line no-console + console.warn( + `Error parsing infrastructure features from PackageManifest "${packageManifest.metadata.name}":`, + error, + ), + }); + + return infrastructureFeatures?.length ? : <>-; +}; + +type OperatorInfrastructureFeaturesProps = { + packageManifest: PackageManifestKind; + clusterIsAWSSTS: boolean; + clusterIsAzureWIF: boolean; + clusterIsGCPWIF: boolean; +}; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-repository.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-repository.tsx new file mode 100644 index 00000000000..16acbb2e2da --- /dev/null +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-repository.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { ExternalLink } from '@console/shared/src/components/links/ExternalLink'; +import { useCurrentChannel } from '../../hooks/useCurrentChannel'; +import { PackageManifestKind } from '../../types'; + +export const OperatorRepository: React.FC = ({ packageManifest }) => { + const currentChannel = useCurrentChannel(packageManifest); + const repository = currentChannel?.currentCSVDesc?.annotations?.repository; + + return repository ? : <>-; +}; + +type OperatorRepositoryProps = { + packageManifest: PackageManifestKind; +}; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-support.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-support.tsx new file mode 100644 index 00000000000..de950796807 --- /dev/null +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-support.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExternalLink } from '@console/shared/src/components/links/ExternalLink'; +import { useCurrentChannel } from '../../hooks/useCurrentChannel'; +import { PackageManifestKind } from '../../types'; +import { getSupportWorkflowUrl } from './operator-hub-utils'; + +export const OperatorSupport: React.FC = ({ packageManifest }) => { + const { t } = useTranslation('olm'); + const currentChannel = useCurrentChannel(packageManifest); + const currentCSVAnnotations = currentChannel?.currentCSVDesc?.annotations ?? {}; + const support = currentCSVAnnotations?.support; + const marketplaceSupportWorkflow = + currentCSVAnnotations?.['marketplace.openshift.io/support-workflow']; + const supportWorkflowUrl = getSupportWorkflowUrl(marketplaceSupportWorkflow); + + return supportWorkflowUrl ? ( + {t('Get support')} + ) : ( + <>{support || '-'} + ); +}; + +type OperatorSupportProps = { + packageManifest: PackageManifestKind; +}; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-valid-subscriptions.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-valid-subscriptions.tsx new file mode 100644 index 00000000000..e9a4d6d715b --- /dev/null +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-valid-subscriptions.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { PlainList } from '@console/shared/src'; +import { useCurrentChannel } from '../../hooks/useCurrentChannel'; +import { PackageManifestKind } from '../../types'; +import { getValidSubscription } from './operator-hub-utils'; +import type { CSVAnnotations } from './index'; + +export const OperatorValidSubscriptions: React.FC = ({ + packageManifest, +}) => { + const currentChannel = useCurrentChannel(packageManifest); + const currentCSVAnnotations: CSVAnnotations = currentChannel?.currentCSVDesc?.annotations ?? {}; + + const [validSubscription] = getValidSubscription(currentCSVAnnotations, { + onError: (error) => + // eslint-disable-next-line no-console + console.warn( + `Error parsing valid subscription from PackageManifest "${packageManifest.metadata.name}":`, + error, + ), + }); + + return validSubscription?.length ? : <>-; +}; + +type OperatorValidSubscriptionsProps = { + packageManifest: PackageManifestKind; +}; diff --git a/frontend/packages/operator-lifecycle-manager/src/hooks/useCurrentCSVDescription.tsx b/frontend/packages/operator-lifecycle-manager/src/hooks/useCurrentCSVDescription.tsx index e0ff6fdd458..7a0c88bad1d 100644 --- a/frontend/packages/operator-lifecycle-manager/src/hooks/useCurrentCSVDescription.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/hooks/useCurrentCSVDescription.tsx @@ -1,12 +1,20 @@ -import { getQueryArgument } from '@console/internal/components/utils'; +import { useSearchParams } from 'react-router-dom-v5-compat'; import { CSVDescription, PackageManifestKind } from '../types'; -export const useCurrentCSVDescription: UseCurrentCSVDescription = (packageManifest) => { - const installChannel = getQueryArgument('channel') ?? packageManifest?.status?.defaultChannel; +export const useCurrentCSVDescription: UseCurrentCSVDescription = ( + packageManifest, + selectedChannel?, +) => { + const [searchParams] = useSearchParams(); + const installChannel = + selectedChannel ?? searchParams.get('channel') ?? packageManifest?.status?.defaultChannel; const currentChannel = packageManifest?.status.channels.find((ch) => ch.name === installChannel) ?? packageManifest?.status.channels[0]; return currentChannel?.currentCSVDesc; }; -type UseCurrentCSVDescription = (packageManifest: PackageManifestKind) => CSVDescription; +type UseCurrentCSVDescription = ( + packageManifest: PackageManifestKind, + selectedChannel?: string, +) => CSVDescription; diff --git a/frontend/packages/operator-lifecycle-manager/src/hooks/useCurrentChannel.tsx b/frontend/packages/operator-lifecycle-manager/src/hooks/useCurrentChannel.tsx new file mode 100644 index 00000000000..1281e4502e3 --- /dev/null +++ b/frontend/packages/operator-lifecycle-manager/src/hooks/useCurrentChannel.tsx @@ -0,0 +1,22 @@ +import { useSearchParams } from 'react-router-dom-v5-compat'; +import { PackageManifestKind } from '../types'; + +/** + * Returns the currently selected channel based on URL query parameters, + * falling back to the default channel or the first available channel. + */ +export const useCurrentChannel = ( + packageManifest: PackageManifestKind, +): PackageManifestKind['status']['channels'][number] | undefined => { + const [searchParams] = useSearchParams(); + const selectedChannel = + searchParams.get('channel') || + packageManifest?.status?.defaultChannel || + packageManifest?.status?.channels?.[0]?.name; + + const currentChannel = packageManifest?.status?.channels?.find( + (ch) => ch.name === selectedChannel, + ); + + return currentChannel; +}; diff --git a/frontend/packages/operator-lifecycle-manager/src/hooks/useOperatorCatalogItems.tsx b/frontend/packages/operator-lifecycle-manager/src/hooks/useOperatorCatalogItems.tsx index 366a087badf..ea813488047 100644 --- a/frontend/packages/operator-lifecycle-manager/src/hooks/useOperatorCatalogItems.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/hooks/useOperatorCatalogItems.tsx @@ -8,9 +8,7 @@ import { CatalogItem, CatalogItemBadge, } from '@console/dynamic-plugin-sdk/src/lib-core'; -import { parseList, PlainList, strConcat } from '@console/shared/src'; -import { Timestamp } from '@console/shared/src/components/datetime/Timestamp'; -import { ExternalLink } from '@console/shared/src/components/links/ExternalLink'; +import { parseList, strConcat } from '@console/shared/src'; import { iconFor } from '../components'; import { subscriptionFor } from '../components/operator-group'; import { @@ -20,23 +18,26 @@ import { InfrastructureFeature, TokenizedAuthProvider, } from '../components/operator-hub/index'; +import { OperatorCapability } from '../components/operator-hub/operator-capability'; import { OperatorVersionSelect, OperatorChannelSelect, } from '../components/operator-hub/operator-channel-version-select'; -import { - CapabilityLevel, - OperatorDescription, -} from '../components/operator-hub/operator-hub-item-details'; +import { OperatorContainerImage } from '../components/operator-hub/operator-container-image'; +import { OperatorCreatedAt } from '../components/operator-hub/operator-created-at'; +import { OperatorDescription } from '../components/operator-hub/operator-hub-item-details'; import { getInfrastructureFeatures, getPackageSource, - getSupportWorkflowUrl, getValidSubscription, isAWSSTSCluster, isAzureWIFCluster, isGCPWIFCluster, } from '../components/operator-hub/operator-hub-utils'; +import { OperatorInfrastructureFeatures } from '../components/operator-hub/operator-infrastructure-features'; +import { OperatorRepository } from '../components/operator-hub/operator-repository'; +import { OperatorSupport } from '../components/operator-hub/operator-support'; +import { OperatorValidSubscriptions } from '../components/operator-hub/operator-valid-subscriptions'; import { PackageManifestModel, SubscriptionModel } from '../models'; import { PackageManifestKind } from '../types'; import { clusterServiceVersionFor } from '../utils/clusterserviceversions'; @@ -249,8 +250,6 @@ export const useOperatorCatalogItems = () => { ? `/k8s/ns/${subscription.metadata.namespace}/${SubscriptionModel.plural}/${subscription.metadata.name}?showDelete=true` : null; - const supportWorkflowUrl = getSupportWorkflowUrl(marketplaceSupportWorkflow); - const cta = installed && uninstallLink ? { @@ -376,38 +375,40 @@ export const useOperatorCatalogItems = () => { }, { label: t('Capability level'), - value: , + value: , }, { label: t('Source'), value: source || '-' }, { label: t('Provider'), value: provider || '-' }, { label: t('Infrastructure features'), - value: infrastructureFeatures?.length ? ( - - ) : ( - '-' + value: ( + ), }, { label: t('Valid subscriptions'), - value: validSubscription?.length ? : '-', + value: , }, { label: t('Repository'), - value: repository ? : '-', + value: , + }, + { + label: t('Container image'), + value: , }, - { label: t('Container image'), value: containerImage || '-' }, { label: t('Created at'), - value: createdAt ? : '-', + value: , }, { label: t('Support'), - value: supportWorkflowUrl ? ( - {t('Get support')} - ) : ( - support || '-' - ), + value: , }, ], descriptions: [ @@ -416,7 +417,6 @@ export const useOperatorCatalogItems = () => {