From 94cec01e099601db1c38f8f9c5d569f95f9e33d0 Mon Sep 17 00:00:00 2001 From: Krish Agarwal Date: Mon, 11 Aug 2025 09:55:43 -0400 Subject: [PATCH 1/5] 70% of SNC in package/olm fixed frontend/packages/operator-lifecycle-manager/src/components/deprecated-operator-warnings/deprecated-operator-warnings.tsx frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx frontend/packages/operator-lifecycle-manager/src/status/csv-status.ts modified: frontend/packages/operator-lifecycle-manager/mocks.ts modified: frontend/packages/operator-lifecycle-manager/src/components/catalog-source.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/dashboard/utils.ts modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/common.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/index.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/index.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/install-plan.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/delete-catalog-source-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/disable-default-source-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/edit-default-sources-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-preview-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operator-group.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operator-group.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/package-manifest.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/resource-sections.tsx modified: frontend/packages/operator-lifecycle-manager/src/utils/clusterserviceversions.ts modified: frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersion.tsx modified: frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersionPath.tsx modified: frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersions.tsx --- .../operator-lifecycle-manager/mocks.ts | 20 +++--- .../src/components/catalog-source.spec.tsx | 45 +++++++----- .../src/components/catalog-source.tsx | 69 +++++++++++-------- .../src/components/dashboard/csv-status.tsx | 6 +- .../src/components/dashboard/utils.ts | 8 +-- .../src/components/descriptors/common.tsx | 27 ++++++-- .../src/components/descriptors/index.tsx | 14 ++-- .../src/components/index.tsx | 14 ++-- .../src/components/install-plan.tsx | 38 +++++----- .../modals/delete-catalog-source-modal.tsx | 6 +- .../modals/disable-default-source-modal.tsx | 2 +- .../modals/edit-default-sources-modal.tsx | 2 +- .../installplan-approval-modal.spec.tsx | 4 +- .../modals/installplan-approval-modal.tsx | 2 +- .../modals/installplan-preview-modal.tsx | 4 +- .../subscription-channel-modal.spec.tsx | 2 +- .../modals/subscription-channel-modal.tsx | 2 +- .../src/components/operator-group.spec.tsx | 58 ++++++++++------ .../src/components/operator-group.tsx | 6 +- .../src/components/operator-install-page.tsx | 60 +++++++++------- .../src/components/package-manifest.spec.tsx | 4 +- .../src/components/package-manifest.tsx | 26 +++---- .../src/components/subscription.spec.tsx | 25 ++++--- .../src/components/subscription.tsx | 38 +++++----- .../TopologyOperatorBackedResources.tsx | 23 ++++--- .../topology/sidebar/resource-sections.tsx | 2 +- .../src/utils/clusterserviceversions.ts | 2 +- .../src/utils/useClusterServiceVersion.tsx | 6 +- .../utils/useClusterServiceVersionPath.tsx | 6 +- .../src/utils/useClusterServiceVersions.tsx | 12 ++-- 30 files changed, 307 insertions(+), 226 deletions(-) diff --git a/frontend/packages/operator-lifecycle-manager/mocks.ts b/frontend/packages/operator-lifecycle-manager/mocks.ts index b0973ce4cd0..19973394137 100644 --- a/frontend/packages/operator-lifecycle-manager/mocks.ts +++ b/frontend/packages/operator-lifecycle-manager/mocks.ts @@ -47,7 +47,7 @@ export const testClusterServiceVersion: ClusterServiceVersionKind = { name: 'testapp', uid: 'c02c0a8f-88e0-11e7-851b-080027b424ef', creationTimestamp: '2017-09-20T18:19:49Z', - deletionTimestamp: null, + deletionTimestamp: undefined, namespace: 'default', }, spec: { @@ -250,10 +250,10 @@ export const testOwnedResourceInstance: K8sResourceKind = { creationTimestamp: '2005-02-20T18:13:42Z', ownerReferences: [ { - name: testResourceInstance.metadata.name, + name: testResourceInstance.metadata?.name || '', kind: 'TestResource', - apiVersion: testResourceInstance.apiVersion, - uid: testResourceInstance.metadata.uid, + apiVersion: testResourceInstance.apiVersion || '', + uid: testResourceInstance.metadata?.uid || '', }, ], }, @@ -359,14 +359,14 @@ export const testOperatorDeployment: K8sResourceKind = { apiVersion: 'apps/v1beta2', kind: 'Deployment', metadata: { - namespace: testClusterServiceVersion.metadata.namespace, + namespace: testClusterServiceVersion.metadata?.namespace || '', name: 'test-operator', ownerReferences: [ { - name: testClusterServiceVersion.metadata.name, - uid: testClusterServiceVersion.metadata.uid, + name: testClusterServiceVersion.metadata?.name || '', + uid: testClusterServiceVersion.metadata?.uid || '', kind: testClusterServiceVersion.kind, - apiVersion: testClusterServiceVersion.apiVersion, + apiVersion: testClusterServiceVersion.apiVersion || '', }, ], }, @@ -772,7 +772,7 @@ export const operatorHubListPageProps = { }; export const operatorHubTileViewPageProps = { - items: [ + items: ([ { obj: amqPackageManifest, installState: 'Installed', @@ -893,7 +893,7 @@ export const operatorHubTileViewPageProps = { infrastructure: infra, authentication: auth, }, - ] as OperatorHubItem[], + ] as unknown) as OperatorHubItem[], openOverlay: null, }; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.spec.tsx index 8c557b267a6..205f100db89 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.spec.tsx @@ -33,7 +33,7 @@ jest.mock('react-router-dom-v5-compat', () => ({ useLocation: jest.fn(), })); -describe(CatalogSourceDetails.displayName, () => { +describe(CatalogSourceDetails.displayName || '', () => { let wrapper: ShallowWrapper; let obj: CatalogSourceDetailsProps['obj']; @@ -43,15 +43,16 @@ describe(CatalogSourceDetails.displayName, () => { }); it('renders name and publisher of the catalog', () => { - expect(wrapper.find(DetailsItem).at(1).props().obj.spec.displayName).toEqual( + expect(wrapper.find(DetailsItem).at(1).props().obj?.spec?.displayName).toEqual( obj.spec.displayName, ); - - expect(wrapper.find(DetailsItem).at(2).props().obj.spec.publisher).toEqual(obj.spec.publisher); + expect(wrapper.find(DetailsItem).at(2).props().obj?.spec?.publisher).toEqual( + obj.spec.publisher, + ); }); }); -describe(CatalogSourceDetailsPage.displayName, () => { +describe(CatalogSourceDetailsPage.displayName || '', () => { let wrapper: ShallowWrapper; beforeEach(() => { @@ -65,13 +66,13 @@ describe(CatalogSourceDetailsPage.displayName, () => { const detailsPage = wrapper.find(DetailsPage); const { pages } = detailsPage.props(); - expect(pages.length).toEqual(3); - expect(pages[0].nameKey).toEqual(`public~Details`); - expect(pages[1].nameKey).toEqual(`public~YAML`); - expect(pages[2].nameKey).toEqual(`olm~Operators`); + expect(pages?.length).toEqual(3); + expect(pages?.[0]?.nameKey).toEqual(`public~Details`); + expect(pages?.[1]?.nameKey).toEqual(`public~YAML`); + expect(pages?.[2]?.nameKey).toEqual(`olm~Operators`); - expect(pages[0].component).toEqual(CatalogSourceDetails); - expect(pages[2].component).toEqual(CatalogSourceOperatorsPage); + expect(pages?.[0]?.component).toEqual(CatalogSourceDetails); + expect(pages?.[2]?.component).toEqual(CatalogSourceOperatorsPage); expect(wrapper.find(DetailsPage).props().resources).toEqual([ { @@ -84,16 +85,18 @@ describe(CatalogSourceDetailsPage.displayName, () => { }); }); -describe(CreateSubscriptionYAML.displayName, () => { +describe(CreateSubscriptionYAML.displayName || '', () => { let wrapper: ShallowWrapper; beforeEach(() => { jest .spyOn(Router, 'useParams') - .mockReturnValue({ ns: 'default', pkgName: testPackageManifest.metadata.name }); + .mockReturnValue({ ns: 'default', pkgName: testPackageManifest.metadata?.name || '' }); jest.spyOn(Router, 'useLocation').mockReturnValue({ ...window.location, - search: `?pkg=${testPackageManifest.metadata.name}&catalog=ocs&catalogNamespace=default`, + search: `?pkg=${ + testPackageManifest.metadata?.name || '' + }&catalog=ocs&catalogNamespace=default`, }); wrapper = shallow(); }); @@ -103,7 +106,7 @@ describe(CreateSubscriptionYAML.displayName, () => { { kind: referenceForModel(PackageManifestModel), isList: false, - name: testPackageManifest.metadata.name, + name: testPackageManifest.metadata?.name || '', namespace: 'default', prop: 'packageManifest', }, @@ -141,12 +144,16 @@ describe(CreateSubscriptionYAML.displayName, () => { .dive(); const subTemplate = safeLoad(createYAML.props().template); - window.location.search = `?pkg=${testPackageManifest.metadata.name}&catalog=ocs&catalogNamespace=default`; + window.location.search = `?pkg=${ + testPackageManifest.metadata?.name || '' + }&catalog=ocs&catalogNamespace=default`; expect(subTemplate.kind).toContain(SubscriptionModel.kind); - expect(subTemplate.spec.name).toEqual(testPackageManifest.metadata.name); - expect(subTemplate.spec.channel).toEqual(testPackageManifest.status.channels[0].name); - expect(subTemplate.spec.startingCSV).toEqual(testPackageManifest.status.channels[0].currentCSV); + expect(subTemplate.spec.name).toEqual(testPackageManifest.metadata?.name || ''); + expect(subTemplate.spec.channel).toEqual(testPackageManifest.status?.channels?.[0]?.name || ''); + expect(subTemplate.spec.startingCSV).toEqual( + testPackageManifest.status?.channels?.[0]?.currentCSV || '', + ); expect(subTemplate.spec.source).toEqual('ocs'); }); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx b/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx index d1db968b423..6fecb5c32b5 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.tsx @@ -114,8 +114,8 @@ const getOperatorCount = ( ): number => packageManifests.filter( (p) => - p.status?.catalogSource === catalogSource.metadata.name && - p.status?.catalogSourceNamespace === catalogSource.metadata.namespace, + p.status?.catalogSource === catalogSource.metadata?.name && + p.status?.catalogSourceNamespace === catalogSource.metadata?.namespace, ).length; const getEndpoint = (catalogSource: CatalogSourceKind): React.ReactNode => { @@ -124,7 +124,7 @@ const getEndpoint = (catalogSource: CatalogSourceKind): React.ReactNode => { ); } @@ -140,9 +140,9 @@ export const CatalogSourceDetails: React.FC = ({ const operatorCount = getOperatorCount(catalogSource, packageManifests); const catsrcNamespace = - catalogSource.metadata.namespace === DEFAULT_SOURCE_NAMESPACE + catalogSource.metadata?.namespace === DEFAULT_SOURCE_NAMESPACE ? 'Cluster wide' - : catalogSource.metadata.namespace; + : catalogSource.metadata?.namespace; return !_.isEmpty(catalogSource) ? ( @@ -220,7 +220,10 @@ export const CatalogSourceDetailsPage: React.FC = (props) => { ); const menuActions = isDefaultSource - ? [Kebab.factory.Edit, () => disableSourceModal(OperatorHubModel, operatorHub, params.name)] + ? [ + Kebab.factory.Edit, + () => disableSourceModal(OperatorHubModel, operatorHub, params.name || ''), + ] : Kebab.factory.common; return ( @@ -273,14 +276,14 @@ export const CreateSubscriptionYAML: React.FC = (props) => { apiVersion: ${SubscriptionModel.apiGroup}/${SubscriptionModel.apiVersion} kind: ${SubscriptionModel.kind}, metadata: - generateName: ${pkg.metadata.name}- + generateName: ${pkg.metadata?.name || ''}- namespace: default spec: source: ${new URLSearchParams(location.search).get('catalog')} sourceNamespace: ${new URLSearchParams(location.search).get('catalogNamespace')} - name: ${pkg.metadata.name} - startingCSV: ${channel.currentCSV} - channel: ${channel.name} + name: ${pkg.metadata?.name || ''} + startingCSV: ${channel?.currentCSV || ''} + channel: ${channel?.name || ''} `; return ( @@ -355,14 +358,14 @@ const CatalogSourceTableRow: React.FC> {source ? ( ) : ( name )} - + {status} {publisher} @@ -459,19 +462,19 @@ const CatalogSourceList: React.FC = (props) => { }; const DisabledPopover: React.FC = ({ operatorHub, sourceName }) => { - const [visible, setVisible] = React.useState(null); + const [visible, setVisible] = React.useState(null); const close = React.useCallback(() => { setVisible(false); }, []); const onClickEnable = React.useCallback( - () => enableSource(OperatorHubModel, operatorHub, sourceName).callback().then(close), + () => enableSource(OperatorHubModel, operatorHub, sourceName)?.callback?.().then(close), [close, operatorHub, sourceName], ); const { t } = useTranslation(); return ( } > @@ -488,7 +491,7 @@ const DisabledPopover: React.FC = ({ operatorHub, sourceNa }; const getRegistryPollInterval = (catalogSource: CatalogSourceKind): string => { - return catalogSource.spec?.updateStrategy?.registryPoll?.interval; + return catalogSource.spec?.updateStrategy?.registryPoll?.interval || ''; }; const flatten = ({ @@ -499,8 +502,8 @@ const flatten = ({ const defaultSources: CatalogSourceTableRowObj[] = _.map( operatorHub.status?.sources, (defaultSource) => { - const catalogSource = _.find(catalogSources.data, { - metadata: { name: defaultSource.name, namespace: DEFAULT_SOURCE_NAMESPACE }, + const catalogSource = _.find(catalogSources?.data, { + metadata: { name: defaultSource.name || '', namespace: DEFAULT_SOURCE_NAMESPACE }, }); const catalogSourceExists = !_.isEmpty(catalogSource); return { @@ -518,25 +521,31 @@ const flatten = ({ operatorHub, ...(catalogSourceExists && { source: catalogSource, - endpoint: getEndpoint(catalogSource), - operatorCount: getOperatorCount(catalogSource, packageManifests.data), - publisher: catalogSource.spec.publisher, - registryPollInterval: getRegistryPollInterval(catalogSource), - status: catalogSource.status?.connectionState?.lastObservedState, + endpoint: getEndpoint(catalogSource || ({} as CatalogSourceKind)), + operatorCount: getOperatorCount( + catalogSource || ({} as CatalogSourceKind), + packageManifests?.data || [], + ), + publisher: catalogSource?.spec?.publisher, + registryPollInterval: getRegistryPollInterval(catalogSource || ({} as CatalogSourceKind)), + status: catalogSource?.status?.connectionState?.lastObservedState, }), }; }, ); - const customSources: CatalogSourceTableRowObj[] = _.map(catalogSources.data, (source) => ({ + const customSources: CatalogSourceTableRowObj[] = _.map(catalogSources?.data, (source) => ({ availability: - source.metadata.namespace === DEFAULT_SOURCE_NAMESPACE + source?.metadata?.namespace === DEFAULT_SOURCE_NAMESPACE ? i18n.t('olm~Cluster wide') - : source.metadata.namespace, + : source?.metadata?.namespace, endpoint: getEndpoint(source), - name: source.metadata.name, - namespace: source.metadata.namespace, - operatorCount: getOperatorCount(source, packageManifests.data), + name: source?.metadata?.name || '', + namespace: source?.metadata?.namespace || '', + operatorCount: getOperatorCount( + source || ({} as CatalogSourceKind), + packageManifests?.data || [], + ), operatorHub, publisher: source.spec.publisher, registryPollInterval: getRegistryPollInterval(source), diff --git a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx index 2a4f5c1aa39..cd8ac2caf0d 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx @@ -16,8 +16,8 @@ import './csv-status.scss'; const ClusterServiceVersionRow: React.FC> = ({ operatorStatus, }) => { - const { name, namespace } = operatorStatus.operators[0].metadata; - const { displayName } = operatorStatus.operators[0].spec; + const { name, namespace } = operatorStatus.operators[0]?.metadata || {}; + const { displayName } = operatorStatus.operators[0]?.spec || {}; const to = operatorStatus.operators.length > 1 ? `${resourcePathFromModel(ClusterServiceVersionModel)}?name=${name}` @@ -28,7 +28,7 @@ const ClusterServiceVersionRow: React.FC - + {displayName || name} diff --git a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/utils.ts b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/utils.ts index 481657e1a03..6044009ec67 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/utils.ts +++ b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/utils.ts @@ -37,7 +37,7 @@ const getOperatorStatus = ( ) { return { ...healthStateMapping[HealthState.UPDATING], - title: subscriptionStatus.title, + title: subscriptionStatus.title || '', }; } if ( @@ -46,12 +46,12 @@ const getOperatorStatus = ( ) { return { ...healthStateMapping[HealthState.UPGRADABLE], - title: subscriptionStatus.title, + title: subscriptionStatus.title || '', }; } return { ...healthStateMapping[operatorHealth], - title: csvStatus.title, + title: csvStatus.title || '', }; }; @@ -69,7 +69,7 @@ export const getClusterServiceVersionsWithStatuses: GetOperatorsWithStatuses { const grouppedOperators = _.groupBy( resources.clusterServiceVersions.data as ClusterServiceVersionKind[], - (o) => o.metadata.name, + (o) => o.metadata?.name || '', ); return _.values(grouppedOperators).map((operators) => getOperatorsStatus(operators, (csv) => diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/common.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/common.tsx index 964d279a2f8..722ef974bd2 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/common.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/common.tsx @@ -38,7 +38,12 @@ export const DefaultCapability: React.FC + {detail} ); @@ -59,7 +64,7 @@ export const K8sResourceLinkCapability: React.FC> return {t('public~None')}; } - const [, suffix] = capability.match(REGEXP_K8S_RESOURCE_SUFFIX) ?? []; + const [, suffix] = capability?.match(REGEXP_K8S_RESOURCE_SUFFIX) ?? []; const gvk = suffix?.replace(/:/g, '~'); if (!_.isString(value)) { // eslint-disable-next-line no-console @@ -70,10 +75,15 @@ export const K8sResourceLinkCapability: React.FC> return null; } - return ; - }, [value, capability, obj.metadata.namespace, t, descriptor]); + return ; + }, [value, capability, obj?.metadata?.namespace, t, descriptor]); return ( - + {detail} ); @@ -90,7 +100,12 @@ export const SecretCapability: React.FC> = ({ const [reveal, setReveal] = React.useState(false); return ( - +
@@ -365,11 +365,11 @@ export const InstallPlanDetails: React.FC = ({ obj }) = {t('olm~Components')} {(obj.spec.clusterServiceVersionNames || []).map((csvName) => ( - {obj.status.phase === 'Complete' ? ( + {obj.status?.phase === 'Complete' ? ( ) : ( @@ -400,7 +400,7 @@ export const InstallPlanDetails: React.FC = ({ obj }) = - + ); @@ -414,7 +414,7 @@ export const InstallPlanPreview: React.FC = ({ const [needsApproval, setNeedsApproval] = React.useState( obj.spec.approval === InstallPlanApproval.Manual && obj.spec.approved === false, ); - const subscription = obj?.metadata?.ownerReferences.find( + const subscription = obj?.metadata?.ownerReferences?.find( (ref) => referenceForOwnerRef(ref) === referenceForModel(SubscriptionModel), ); @@ -442,7 +442,7 @@ export const InstallPlanPreview: React.FC = ({ const canPatchInstallPlans = useAccessReview({ group: InstallPlanModel.apiGroup, resource: InstallPlanModel.plural, - namespace: obj.metadata.namespace, + namespace: obj.metadata?.namespace, verb: 'patch', }); @@ -469,9 +469,9 @@ export const InstallPlanPreview: React.FC = ({ isDisabled={false} onClick={() => history.push( - `/k8s/ns/${obj.metadata.namespace}/${referenceForModel( + `/k8s/ns/${obj.metadata?.namespace}/${referenceForModel( SubscriptionModel, - )}/${subscription.name}?showDelete=true`, + )}/${subscription?.name}?showDelete=true`, ) } > @@ -506,7 +506,7 @@ export const InstallPlanPreview: React.FC = ({ {['Present', 'Created'].includes(step.status) ? ( @@ -560,8 +560,8 @@ export const InstallPlanDetailsPage: React.FC = (props) => { { href: 'components', nameKey: 'olm~Components', component: InstallPlanPreview }, ]} menuActions={[ - ...Kebab.getExtensionsActionsForKind(InstallPlanModel), - ...Kebab.factory.common, + ...(Kebab.getExtensionsActionsForKind(InstallPlanModel) || []), + ...(Kebab.factory.common || []), ]} /> ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/delete-catalog-source-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/delete-catalog-source-modal.tsx index 005f9b5f517..310153486ba 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/delete-catalog-source-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/delete-catalog-source-modal.tsx @@ -21,7 +21,7 @@ const DeleteCatalogSourceModal: React.FC = ({ const { t } = useTranslation(); const [confirmed, setConfirmed] = React.useState(false); const isConfirmed = (e: React.KeyboardEvent) => { - setConfirmed(e.currentTarget.value === resource.metadata.name); + setConfirmed(e.currentTarget.value === resource?.metadata?.name); }; const submit = React.useCallback( @@ -51,7 +51,7 @@ const DeleteCatalogSourceModal: React.FC = ({

Confirm deletion by typing{' '} - {{ name: resource.metadata.name }} below: + {{ name: resource?.metadata?.name }} below:

@@ -66,7 +66,7 @@ const DeleteCatalogSourceModal: React.FC = ({ = ({ = ({ errorMessage={errorMessage} inProgress={inProgress} submitText={t('public~Save')} - cancel={cancel} + cancel={cancel as any} />
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx index b9969d4a94e..5f87cff54d8 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx @@ -70,7 +70,7 @@ describe(InstallPlanApprovalModal.name, () => { .find(Radio) .at(1) .props() - .onChange({ target: { value: InstallPlanApproval.Manual } } as any, true); + .onChange?.({ target: { value: InstallPlanApproval.Manual } } as any, true); wrapper.find('form').simulate('submit', new Event('submit')); }); @@ -87,7 +87,7 @@ describe(InstallPlanApprovalModal.name, () => { .find(Radio) .at(1) .props() - .onChange({ target: { value: InstallPlanApproval.Manual } } as any, true); + .onChange?.({ target: { value: InstallPlanApproval.Manual } } as any, true); wrapper.find('form').simulate('submit', new Event('submit')); }); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.tsx index 7d151fe32fa..5ed2acb35c4 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.tsx @@ -99,7 +99,7 @@ export const InstallPlanApprovalModal: React.FC = diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-preview-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-preview-modal.tsx index 2475a4459a5..9738faffab7 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-preview-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-preview-modal.tsx @@ -25,11 +25,11 @@ const InstallPlanPreview: React.FC = ({ cancel, st /> - + - diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx index 008a757eead..e47d183a1af 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx @@ -91,7 +91,7 @@ describe('SubscriptionChannelModal', () => { .find(Radio) .at(1) .props() - .onChange({ target: { value: 'nightly' } } as any, true); + .onChange?.({ target: { value: 'nightly' } } as any, true); wrapper.find('form').simulate('submit', new Event('submit')); }); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.tsx index a93c9e3c817..6390ead54d9 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.tsx @@ -87,7 +87,7 @@ export const SubscriptionChannelModal: React.FC = diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-group.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-group.spec.tsx index 2313d266a0a..c5a27d8ce71 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-group.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-group.spec.tsx @@ -48,7 +48,7 @@ describe('requireOperatorGroup', () => { describe('subscriptionFor', () => { const pkg = testPackageManifest; - const ns = testSubscription.metadata.namespace; + const ns = testSubscription.metadata?.namespace; let subscriptions: SubscriptionKind[]; let operatorGroups: OperatorGroupKind[]; @@ -59,7 +59,9 @@ describe('subscriptionFor', () => { it('returns nothing if no `Subscriptions` exist for the given package', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }]; + operatorGroups = [ + { ...testOperatorGroup, status: { namespaces: [ns || ''], lastUpdated: '' } }, + ]; expect( subscriptionFor(subscriptions)(operatorGroups)(dummyPackageManifest)(ns), @@ -69,7 +71,7 @@ describe('subscriptionFor', () => { it('returns nothing if no `OperatorGroups` target the given namespace', () => { subscriptions = [testSubscription]; operatorGroups = [ - { ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: null } }, + { ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: '' } }, ]; expect(subscriptionFor(subscriptions)(operatorGroups)(pkg)(ns)).toBeUndefined(); @@ -78,7 +80,7 @@ describe('subscriptionFor', () => { it('returns nothing if no `Subscriptions` share the package namespace', () => { subscriptions = [testSubscription]; operatorGroups = [ - { ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: null } }, + { ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: '' } }, ]; expect( @@ -89,7 +91,7 @@ describe('subscriptionFor', () => { it('returns nothing if no `Subscriptions` share the package catalog source', () => { subscriptions = [testSubscription]; operatorGroups = [ - { ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: null } }, + { ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: '' } }, ]; expect( @@ -99,21 +101,25 @@ describe('subscriptionFor', () => { it('returns nothing if checking for `all-namespaces`', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }]; + operatorGroups = [ + { ...testOperatorGroup, status: { namespaces: [ns || ''], lastUpdated: '' } }, + ]; expect(subscriptionFor(subscriptions)(operatorGroups)(pkg)('')).toBeUndefined(); }); it('returns `Subscription` when it exists in the "global" `OperatorGroup`', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [''], lastUpdated: null } }]; + operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [''], lastUpdated: '' } }]; expect(subscriptionFor(subscriptions)(operatorGroups)(pkg)(ns)).toEqual(testSubscription); }); it('returns `Subscription` when it exists in an `OperatorGroup` that targets given namespace', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }]; + operatorGroups = [ + { ...testOperatorGroup, status: { namespaces: [ns || ''], lastUpdated: '' } }, + ]; expect(subscriptionFor(subscriptions)(operatorGroups)(pkg)(ns)).toEqual(testSubscription); }); @@ -121,7 +127,7 @@ describe('subscriptionFor', () => { describe('installedFor', () => { const pkg = testPackageManifest; - const ns = testSubscription.metadata.namespace; + const ns = testSubscription.metadata?.namespace; let subscriptions: SubscriptionKind[]; let operatorGroups: OperatorGroupKind[]; @@ -132,7 +138,9 @@ describe('installedFor', () => { it('returns false if no `Subscriptions` exist for the given package', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }]; + operatorGroups = [ + { ...testOperatorGroup, status: { namespaces: [ns || ''], lastUpdated: '' } }, + ]; expect(installedFor(subscriptions)(operatorGroups)(dummyPackageManifest)(ns)).toBe(false); }); @@ -140,7 +148,7 @@ describe('installedFor', () => { it('returns false if no `OperatorGroups` target the given namespace', () => { subscriptions = [testSubscription]; operatorGroups = [ - { ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: null } }, + { ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: '' } }, ]; expect(installedFor(subscriptions)(operatorGroups)(pkg)(ns)).toBe(false); @@ -148,35 +156,43 @@ describe('installedFor', () => { it('returns false if checking for `all-namespaces`', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }]; + operatorGroups = [ + { ...testOperatorGroup, status: { namespaces: [ns || ''], lastUpdated: '' } }, + ]; expect(installedFor(subscriptions)(operatorGroups)(pkg)('')).toBe(false); }); it('returns false if `Subscription` is in a different namespace than the given package', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }]; + operatorGroups = [ + { ...testOperatorGroup, status: { namespaces: [ns || ''], lastUpdated: '' } }, + ]; expect(installedFor(subscriptions)(operatorGroups)(dummyPackageManifest)(ns)).toBe(false); }); it('returns false if `Subscription` is from a different catalog source than the given package', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }]; + operatorGroups = [ + { ...testOperatorGroup, status: { namespaces: [ns || ''], lastUpdated: '' } }, + ]; expect(installedFor(subscriptions)(operatorGroups)(dummyPackageManifest)(ns)).toBe(false); }); it('returns true if `Subscription` exists in the "global" `OperatorGroup`', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [''], lastUpdated: null } }]; + operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [''], lastUpdated: '' } }]; expect(installedFor(subscriptions)(operatorGroups)(pkg)(ns)).toBe(true); }); it('returns true if `Subscription` exists in an `OperatorGroup` that targets given namespace', () => { subscriptions = [testSubscription]; - operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }]; + operatorGroups = [ + { ...testOperatorGroup, status: { namespaces: [ns || ''], lastUpdated: '' } }, + ]; expect(installedFor(subscriptions)(operatorGroups)(pkg)(ns)).toBe(true); }); @@ -192,15 +208,15 @@ describe('supports', () => { beforeEach(() => { ownNamespaceGroup = _.cloneDeep(testOperatorGroup); ownNamespaceGroup.status = { - namespaces: [ownNamespaceGroup.metadata.namespace], - lastUpdated: null, + namespaces: [ownNamespaceGroup.metadata?.namespace || ''], + lastUpdated: '', }; singleNamespaceGroup = _.cloneDeep(testOperatorGroup); - singleNamespaceGroup.status = { namespaces: ['test-ns'], lastUpdated: null }; + singleNamespaceGroup.status = { namespaces: ['test-ns'], lastUpdated: '' }; multiNamespaceGroup = _.cloneDeep(testOperatorGroup); - multiNamespaceGroup.status = { namespaces: ['test-ns', 'default'], lastUpdated: null }; + multiNamespaceGroup.status = { namespaces: ['test-ns', 'default'], lastUpdated: '' }; allNamespacesGroup = _.cloneDeep(testOperatorGroup); - allNamespacesGroup.status = { namespaces: [''], lastUpdated: null }; + allNamespacesGroup.status = { namespaces: [''], lastUpdated: '' }; }); it('correctly returns for an Operator that can only run in its own namespace', () => { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-group.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-group.tsx index 30c6209d9eb..a4d5d29fbb9 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-group.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-group.tsx @@ -101,7 +101,7 @@ export const supports = (set: InstallModeSet) => (obj: OperatorGroupKind) => { return false; } if (namespaces.length === 1) { - if (namespaces[0] === obj.metadata.namespace) { + if (namespaces[0] === obj.metadata?.namespace) { return supportedModes.includes(InstallModeType.InstallModeTypeOwnNamespace); } if (namespaces[0] === '') { @@ -117,7 +117,7 @@ export const supports = (set: InstallModeSet) => (obj: OperatorGroupKind) => { } if (namespaces.length > 1) { if ( - namespaces.includes(obj.metadata.namespace) && + namespaces.includes(obj.metadata?.namespace || '') && !supportedModes.includes(InstallModeType.InstallModeTypeOwnNamespace) ) { return false; @@ -152,7 +152,7 @@ export const subscriptionFor = (allSubscriptions: SubscriptionKind[] = []) => ( .find((sub) => allGroups.some( (og) => - og.metadata.namespace === sub.metadata.namespace && + og.metadata?.namespace === sub.metadata?.namespace && (isGlobal(og) || og.status?.namespaces?.includes(ns)), ), ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx index 422c83aeed6..21685368100 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-install-page.tsx @@ -48,6 +48,7 @@ import { SubscriptionKind, InstallPlanKind, PackageManifestKind, + ClusterServiceVersionIcon, } from '../types'; import { ClusterServiceVersionLogo } from './cluster-service-version-logo'; import { InstallPlanPreview, NeedInstallPlanPermissions } from './install-plan'; @@ -68,7 +69,7 @@ const ViewInstalledOperatorsButton: React.FC = ({ names
{namespace ? singleNamespaceText : allNamespacesText} @@ -178,8 +179,8 @@ export const CreateInitializationResourceButton: React.FC {button} @@ -269,14 +270,14 @@ const InstallSucceededMessage: React.FC = ({ {initializationResource && t('olm~ Create the required custom resource to prepare it for use.')} {!initializationLink && !initializationResource && ( @@ -323,7 +324,7 @@ const InstallingMessage: React.FC = ({ namespace, obj }) @@ -351,8 +352,8 @@ const OperatorInstallLogo = ({ subscription }) => { }, selector: { matchLabels: { - 'catalog-namespace': catalogNamespace, - catalog, + 'catalog-namespace': catalogNamespace || '', + catalog: catalog || '', }, }, fieldSelector: `metadata.name=${pkg}`, @@ -366,7 +367,7 @@ const OperatorInstallLogo = ({ subscription }) => { if (loadError || !pkgManifest) { return ( ); @@ -380,7 +381,7 @@ const OperatorInstallLogo = ({ subscription }) => { return ( @@ -392,19 +393,19 @@ const OperatorInstallStatus: React.FC = ({ resources } const { currentCSV, targetNamespace } = useParams(); let loading = true; let status = ''; - let installObj: ClusterServiceVersionKind | InstallPlanKind = + let installObj: ClusterServiceVersionKind | InstallPlanKind | undefined = resources?.clusterServiceVersion?.data; const subscription = resources?.subscription?.data; - status = installObj?.status?.phase; + status = installObj?.status?.phase || ''; if (installObj && status) { loading = false; } else if (subscription) { // There is no ClusterServiceVersion for the package, so look at Subscriptions/InstallPlans loading = false; - status = subscription?.status?.state || null; + status = subscription?.status?.state || ''; const installPlanName = subscription?.status?.installPlanRef?.name || ''; - const installPlan: InstallPlanKind = resources?.installPlans?.data?.find( - (ip) => ip.metadata.name === installPlanName, + const installPlan: InstallPlanKind | undefined = resources?.installPlans?.data?.find( + (ip) => ip.metadata?.name === installPlanName, ); if (installPlan) { installObj = installPlan; @@ -417,7 +418,7 @@ const OperatorInstallStatus: React.FC = ({ resources } installObj?.spec?.approval === 'Manual' && installObj?.spec?.approved === false; const approve = () => { - k8sPatch(InstallPlanModel, installObj, [ + k8sPatch(InstallPlanModel, installObj as InstallPlanKind, [ { op: 'replace', path: '/spec/approved', value: true }, ]).catch((error) => { errorModal({ error: error.toString() }); @@ -447,23 +448,36 @@ const OperatorInstallStatus: React.FC = ({ resources } ); } - let installMessage = ; + let installMessage = ( + + ); if (isStatusFailed) { installMessage = ( - + ); } else if (isApprovalNeeded) { installMessage = ( ); } else if (isStatusSucceeded) { installMessage = ( - + ); } @@ -489,7 +503,7 @@ const OperatorInstallStatus: React.FC = ({ resources }
- +
{indicator}
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx index ac05bce98f0..d7d7671f0fa 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx @@ -66,7 +66,7 @@ describe('PackageManifestTableRow', () => { }); it('renders column for creation timestamp', () => { - const pkgManifestCreationTimestamp = testPackageManifest.metadata.creationTimestamp; + const pkgManifestCreationTimestamp = testPackageManifest.metadata?.creationTimestamp || ''; expect(wrapper.childAt(2).dive().find(Timestamp).props().timestamp).toEqual( `${pkgManifestCreationTimestamp}`, ); @@ -80,7 +80,7 @@ describe('PackageManifestTableRow', () => { wrapper = shallow( , ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.tsx b/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.tsx index e3915da677f..923cfaa6a3d 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.tsx @@ -22,7 +22,7 @@ import { MatchExpression, referenceForModel } from '@console/internal/module/k8s import { OPERATOR_HUB_LABEL } from '@console/shared'; import { Timestamp } from '@console/shared/src/components/datetime/Timestamp'; import { PackageManifestModel, CatalogSourceModel } from '../models'; -import { PackageManifestKind, CatalogSourceKind } from '../types'; +import { PackageManifestKind, CatalogSourceKind, ClusterServiceVersionIcon } from '../types'; import { ClusterServiceVersionLogo } from './cluster-service-version-logo'; import { visibilityLabel, iconFor, defaultChannelFor } from './index'; @@ -68,7 +68,7 @@ export const PackageManifestTableRow: React.FC> = ({ obj: packageManifest, customData }) => { const channel = defaultChannelFor(packageManifest); - const { displayName, version, provider } = channel?.currentCSVDesc; + const { displayName, version, provider } = channel?.currentCSVDesc || {}; return ( <> @@ -76,24 +76,24 @@ export const PackageManifestTableRow: React.FC - {version} ({channel.name}) + {version} ({channel?.name}) - + - {!customData.catalogSource && ( + {!customData?.catalogSource && ( { const { customData } = props; // If the CatalogSource is not present, display PackageManifests along with their CatalogSources (used in PackageManifest Search page) - const TableHeader = customData.catalogSource + const TableHeader = customData?.catalogSource ? PackageManifestTableHeader : PackageManifestTableHeaderWithCatalogSource; @@ -183,12 +183,12 @@ export const PackageManifestsPage: React.FC = (props) { key: 'catalog', operator: 'In', - values: [catalogSource?.metadata.name], + values: [catalogSource?.metadata?.name || ''], }, { key: 'catalog-namespace', operator: 'In', - values: [catalogSource?.metadata.namespace], + values: [catalogSource?.metadata?.namespace || ''], }, ] : []) as MatchExpression[]), diff --git a/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx index c0d599ea1e6..ac0aff906cd 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx @@ -69,10 +69,10 @@ describe('SubscriptionTableRow', () => { it('renders column for subscription name', () => { expect(wrapper.childAt(0).shallow().find(ResourceLink).props().name).toEqual( - subscription.metadata.name, + subscription.metadata?.name, ); expect(wrapper.childAt(0).shallow().find(ResourceLink).props().namespace).toEqual( - subscription.metadata.namespace, + subscription.metadata?.namespace, ); expect(wrapper.childAt(0).shallow().find(ResourceLink).props().kind).toEqual( referenceForModel(SubscriptionModel), @@ -87,13 +87,16 @@ describe('SubscriptionTableRow', () => { it('renders column for namespace name', () => { expect(wrapper.childAt(1).shallow().find(ResourceLink).props().name).toEqual( - subscription.metadata.namespace, + subscription.metadata?.namespace, ); expect(wrapper.childAt(1).shallow().find(ResourceLink).props().kind).toEqual('Namespace'); }); it('renders column for subscription state when update available', () => { - subscription.status.state = SubscriptionState.SubscriptionStateUpgradeAvailable; + subscription.status = { + ...subscription.status, + state: SubscriptionState.SubscriptionStateUpgradeAvailable, + }; wrapper = updateWrapper(); expect(wrapper.childAt(2).find(SubscriptionStatus).shallow().text()).toContain( @@ -106,14 +109,20 @@ describe('SubscriptionTableRow', () => { }); it('renders column for subscription state when update in progress', () => { - subscription.status.state = SubscriptionState.SubscriptionStateUpgradePending; + subscription.status = { + ...subscription.status, + state: SubscriptionState.SubscriptionStateUpgradeAvailable, + }; wrapper = updateWrapper(); expect(wrapper.childAt(2).find(SubscriptionStatus).shallow().text()).toContain('Upgrading'); }); it('renders column for subscription state when no updates available', () => { - subscription.status.state = SubscriptionState.SubscriptionStateAtLatest; + subscription.status = { + ...subscription.status, + state: SubscriptionState.SubscriptionStateAtLatest, + }; wrapper = updateWrapper(); expect(wrapper.childAt(2).find(SubscriptionStatus).shallow().text()).toContain('Up to date'); @@ -137,7 +146,7 @@ describe('SubscriptionsList', () => { data={[]} loaded {...{ [referenceForModel(ClusterServiceVersionModel)]: { data: [] } }} - operatorGroup={null} + operatorGroup={{ loaded: false, data: [] }} />, ); }); @@ -264,7 +273,7 @@ describe('SubscriptionDetails', () => { it('renders link to `ClusterServiceVersion` if installed', () => { const obj = _.cloneDeep(testSubscription); - obj.status = { installedCSV: testClusterServiceVersion.metadata.name }; + obj.status = { installedCSV: testClusterServiceVersion.metadata?.name || '' }; wrapper = wrapper.setProps({ obj, clusterServiceVersions: [testClusterServiceVersion] }); const link = wrapper diff --git a/frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx b/frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx index 19b4755448d..befed5205b6 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/subscription.tsx @@ -109,13 +109,15 @@ export const catalogSourceForSubscription = ( (source) => source?.metadata?.name === subscription?.spec?.source && source?.metadata?.namespace === subscription?.spec?.sourceNamespace, - ); + ) as CatalogSourceKind; export const installedCSVForSubscription = ( clusterServiceVersions: ClusterServiceVersionKind[] = [], subscription: SubscriptionKind, ): ClusterServiceVersionKind => - clusterServiceVersions.find((csv) => csv?.metadata?.name === subscription?.status?.installedCSV); + clusterServiceVersions.find( + (csv) => csv?.metadata?.name === subscription?.status?.installedCSV, + ) as ClusterServiceVersionKind; export const packageForSubscription = ( packageManifests: PackageManifestKind[] = [], @@ -127,13 +129,15 @@ export const packageForSubscription = ( pkg?.status?.packageName === subscription?.spec?.name && pkg?.status?.catalogSource === subscription?.spec?.source && pkg?.status?.catalogSourceNamespace === subscription?.spec?.sourceNamespace, - ); + ) as PackageManifestKind; export const installPlanForSubscription = ( installPlans: InstallPlanKind[] = [], subscription: SubscriptionKind, ): InstallPlanKind => - installPlans.find((ip) => ip?.metadata?.name === subscription?.status?.installPlanRef?.name); + installPlans.find( + (ip) => ip?.metadata?.name === subscription?.status?.installPlanRef?.name, + ) as InstallPlanKind; export const SourceMissingStatus: React.FC = () => { const { t } = useTranslation(); @@ -170,8 +174,8 @@ export const UpgradeApprovalLink: React.FC<{ subscription: SubscriptionKind }> = const { t } = useTranslation(); const to = resourcePathFromModel( InstallPlanModel, - subscription.status.installPlanRef.name, - subscription.metadata.namespace, + subscription.status?.installPlanRef?.name || '', + subscription.metadata?.namespace || '', ); return ( @@ -223,8 +227,8 @@ export const SubscriptionTableRow: React.FC = ({ obj }) => { @@ -424,7 +428,7 @@ export const SubscriptionDetails: React.FC = ({ const { t } = useTranslation(); const { source, sourceNamespace } = obj?.spec ?? {}; const catalogHealth = obj?.status?.catalogHealth?.find( - (ch) => ch.catalogSourceRef.name === source, + (ch) => ch?.catalogSourceRef?.name === source, ); const installedCSV = installedCSVForSubscription(clusterServiceVersions, obj); const installPlan = installPlanForSubscription(installPlans, obj); @@ -464,7 +468,7 @@ export const SubscriptionDetails: React.FC = ({ = ({ - + ); @@ -606,7 +610,7 @@ export const SubscriptionUpdates: React.FC = ({ }, [installPlan, t]); const manualSubscriptionsInNamespace = getManualSubscriptionsInNamespace( subscriptions, - obj.metadata.namespace, + obj.metadata?.namespace || '', ); const { deprecatedChannel } = findDeprecatedOperator(obj); @@ -674,7 +678,7 @@ export const SubscriptionUpdates: React.FC = ({ bodyContent={ } > @@ -705,7 +709,7 @@ export const SubscriptionUpdates: React.FC = ({ {obj?.status?.installedCSV && installedCSV ? ( @@ -718,9 +722,9 @@ export const SubscriptionUpdates: React.FC = ({ obj?.status?.installPlanRef && installPlan ? ( {installPlanPhase} diff --git a/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx b/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx index 6681e7f1eb9..2785c121dc1 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx @@ -4,7 +4,9 @@ import { Link } from 'react-router-dom-v5-compat'; import { TopologyDataObject } from '@console/dynamic-plugin-sdk/src/extensions/topology-types'; import { Firehose, ResourceIcon, StatusBox } from '@console/internal/components/utils'; import { + FirehoseResource, GroupVersionKind, + K8sResourceCommon, K8sResourceKind, modelFor, referenceFor, @@ -15,6 +17,7 @@ import { ClusterServiceVersionKind, ClusterServiceVersionModel, CRDDescription, + ProvidedAPI, providedAPIForReference, } from '@console/operator-lifecycle-manager/src'; import { @@ -44,7 +47,7 @@ const OperatorResources: React.FC = ({ linkForResource, }) => { const { t } = useTranslation(); - const manifestResources = flatten(resources); + const manifestResources = flatten(resources || {}); return ( = ({ }) => { const providedAPI = providedAPIForReference(csv, modelReference); const linkForResource = (obj: K8sResourceKind) => { - return linkForCsvResource(obj, providedAPI, csv.metadata.name); + return linkForCsvResource(obj, providedAPI as ProvidedAPI, csv.metadata?.name || ''); }; const defaultResources = [ 'Deployment', @@ -93,7 +96,7 @@ const OperatorResourcesGetter: React.FC = ({ kind, })) as CRDDescription['resources']); - const firehoseResources = resourcesToGet.reduce((acc, descriptor) => { + const firehoseResources = resourcesToGet?.reduce((acc, descriptor) => { const { name, kind, version } = descriptor; const group = name ? name.substring(name.indexOf('.') + 1) : ''; const reference = group ? referenceForGroupVersionKind(group)(version)(kind) : kind; @@ -107,7 +110,7 @@ const OperatorResourcesGetter: React.FC = ({ optional: true, }); return acc; - }, []); + }, [] as FirehoseResource[]); return ( @@ -131,13 +134,13 @@ const TopologyOperatorBackedResources: React.FC { const { t } = useTranslation(); const { resource } = item; - const { namespace } = resource.metadata; - const reference = referenceFor(resource); - const flatten = flattenCsvResources(resource); + const { namespace } = resource?.metadata || {}; + const reference = referenceFor(resource as K8sResourceCommon); + const flatten = flattenCsvResources(resource as K8sResourceCommon); const getManagedByCSVResourceLink = () => { const model = modelFor(referenceFor(csv)); - const { name } = csv.metadata; - const { kind } = model; + const { name } = csv.metadata || {}; + const { kind } = model || {}; const link = `/k8s/ns/${namespace}/${referenceForModel(ClusterServiceVersionModel)}/${name}`; @@ -165,7 +168,7 @@ const TopologyOperatorBackedResources: React.FC
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/resource-sections.tsx b/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/resource-sections.tsx index 186d4c685a0..7acbdb4f5e1 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/resource-sections.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/resource-sections.tsx @@ -13,7 +13,7 @@ import { OperatorGroupData } from './types'; const ResourceSection: React.FC<{ item: TopologyDataObject }> = ({ item }) => { const { resource, data } = item; - const { namespace } = resource.metadata; + const { namespace } = resource?.metadata || {}; const { csvName } = data; const resourcesList = React.useMemo(() => { diff --git a/frontend/packages/operator-lifecycle-manager/src/utils/clusterserviceversions.ts b/frontend/packages/operator-lifecycle-manager/src/utils/clusterserviceversions.ts index 73d519f233f..65125428a5b 100644 --- a/frontend/packages/operator-lifecycle-manager/src/utils/clusterserviceversions.ts +++ b/frontend/packages/operator-lifecycle-manager/src/utils/clusterserviceversions.ts @@ -13,7 +13,7 @@ export const isCopiedCSV = (obj: K8sResourceKind): boolean => export const isStandaloneCSV = (obj: K8sResourceKind): boolean => isCSV(obj) && - (obj.metadata.annotations?.[OLMAnnotation.OperatorType] !== NON_STANDALONE_ANNOTATION_VALUE || + (obj.metadata?.annotations?.[OLMAnnotation.OperatorType] !== NON_STANDALONE_ANNOTATION_VALUE || obj.status?.phase === ClusterServiceVersionPhase.CSVPhaseFailed); export const clusterServiceVersionFor = (clusterServiceVersions: ClusterServiceVersionKind[]) => ( diff --git a/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersion.tsx b/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersion.tsx index 85ea7a10e5c..fb49929d576 100644 --- a/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersion.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersion.tsx @@ -40,7 +40,11 @@ export const useClusterServiceVersion = ( return useMemo(() => { if (copiedCSVsDisabled && Boolean(namespacedCSVLoadError)) { - return [isCopiedCSV(globalCSV) ? globalCSV : null, globalCSVLoaded, globalCSVLoadError]; + return [ + isCopiedCSV(globalCSV) ? globalCSV : ({} as ClusterServiceVersionKind), + globalCSVLoaded, + globalCSVLoadError, + ]; } return [namespacedCSV, namespacedCSVLoaded, namespacedCSVLoadError]; }, [ diff --git a/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersionPath.tsx b/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersionPath.tsx index b2a0c4371e4..5fe69d70ae0 100644 --- a/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersionPath.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersionPath.tsx @@ -10,10 +10,10 @@ export const useClusterServiceVersionPath = (csv: ClusterServiceVersionKind): st // Don't link to csv in 'openshift' namespace when copiedCSVsDisabled and in another namespace if ( window.SERVER_FLAGS.copiedCSVsDisabled && - csv.metadata.namespace === 'openshift' && // Is global csv + csv.metadata?.namespace === 'openshift' && // Is global csv activeNamespace !== ALL_NAMESPACES_KEY ) { - return resourcePath(csvReference, csv.metadata.name, activeNamespace); + return resourcePath(csvReference, csv.metadata?.name || '', activeNamespace || '') || ''; } - return resourceObjPath(csv, csvReference); + return resourceObjPath(csv, csvReference) || ''; }; diff --git a/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersions.tsx b/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersions.tsx index 5ecc3151868..4df8c195455 100644 --- a/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersions.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersions.tsx @@ -61,14 +61,14 @@ const normalizeClusterServiceVersions = ( [], ) .map((desc) => { - const { creationTimestamp } = desc.csv.metadata; - const uid = `${desc.csv.metadata.uid}-${desc.displayName}`; + const { creationTimestamp } = desc.csv.metadata || {}; + const uid = `${desc.csv.metadata?.uid || ''}-${desc.displayName}`; const { description } = desc; const provider = desc.csv.spec.provider?.name; const operatorName = desc.csv.spec.displayName; const supportUrl = - desc.csv.metadata.annotations?.['marketplace.openshift.io/support-workflow']; - const markdownDescription = formatTileDescription(desc.csv.spec.description); + desc.csv.metadata?.annotations?.['marketplace.openshift.io/support-workflow']; + const markdownDescription = formatTileDescription(desc.csv.spec.description || ''); const longDescription = t( 'olm~This resource is provided by {{operatorName}}, a Kubernetes Operator enabled by the Operator Lifecycle Manager.', { operatorName }, @@ -121,13 +121,13 @@ const normalizeClusterServiceVersions = ( operatorName, }, icon: { - class: null, + class: undefined, url: getImageForCSVIcon(desc.csv.spec.icon?.[0]), }, cta: { label: t('public~Create'), href: `/k8s/ns/${namespace}/clusterserviceversions/${ - desc.csv.metadata.name + desc.csv.metadata?.name || '' }/${referenceForProvidedAPI(desc)}/~new`, }, details: { From 9d732315ffa1fb6d7cda6481c66a5ad4e4be99ff Mon Sep 17 00:00:00 2001 From: Krish Agarwal Date: Mon, 11 Aug 2025 22:25:01 -0400 Subject: [PATCH 2/5] Fixed all SNC errors in olm and olm-v1 frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx modified: frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx modified: frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts modified: frontend/packages/operator-lifecycle-manager/mocks.ts modified: frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/affinity.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/index.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/phase.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/pods.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/install-plan.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/index.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/utils.spec.ts modified: frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-page.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts modified: frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx pick 4d8a0efa6a 70% of SNC in package/olm fixed pick a35a41d13c Fixed all SNC errors in olm and olm-v1 modified: frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx modified: frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts modified: frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts modified: frontend/packages/operator-lifecycle-manager/mocks.ts modified: frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/affinity.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/index.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/phase.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/pods.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/install-plan.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/index.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operand/utils.spec.ts modified: frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-page.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts modified: frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx modified: frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx --- .../src/contexts/types.ts | 2 +- ...seExtensionCatalogDatabaseContextValues.ts | 4 +- .../src/database/injest.ts | 6 +- .../src/database/jsonl.ts | 3 + .../src/fbc/bundles.ts | 2 +- .../src/fbc/catalog-item.tsx | 6 +- .../src/fbc/metadata.ts | 2 +- .../src/fbc/packages.ts | 2 +- .../hooks/useExtensionCatalogCategories.ts | 17 ++- .../src/hooks/useExtensionCatalogItems.ts | 23 +-- .../operator-lifecycle-manager/mocks.ts | 4 + .../components/clusterserviceversion.spec.tsx | 144 ++++++++++-------- .../src/components/clusterserviceversion.tsx | 71 ++++----- .../components/descriptors/spec/affinity.tsx | 6 +- .../descriptors/spec/index.spec.tsx | 6 +- .../src/components/descriptors/spec/index.tsx | 106 +++++++++---- .../spec/resource-requirements.spec.tsx | 34 +++-- .../components/descriptors/status/index.tsx | 25 ++- .../descriptors/status/phase.spec.tsx | 4 +- .../descriptors/status/pods.spec.tsx | 2 +- .../src/components/install-plan.spec.tsx | 40 ++--- .../src/components/k8s-resource.tsx | 46 +++--- .../modals/uninstall-operator-modal.spec.tsx | 2 +- .../modals/uninstall-operator-modal.tsx | 46 +++--- .../modals/update-strategy-modal.tsx | 2 +- .../operand/DEPRECATED_operand-form.tsx | 80 ++++++---- .../operand/create-operand.spec.tsx | 19 ++- .../src/components/operand/create-operand.tsx | 12 +- .../src/components/operand/index.spec.tsx | 83 +++++----- .../src/components/operand/index.tsx | 56 +++---- .../src/components/operand/operand-form.tsx | 4 +- .../src/components/operand/operand-link.tsx | 2 +- .../src/components/operand/utils.spec.ts | 16 +- .../operator-hub/operator-hub-items.tsx | 77 +++++----- .../operator-hub/operator-hub-page.tsx | 51 +++++-- .../operator-hub/operator-hub-utils.spec.ts | 20 +-- .../operator-hub/operator-hub.spec.tsx | 19 ++- .../src/components/package-manifest.spec.tsx | 2 +- .../src/components/subscription.spec.tsx | 4 +- 39 files changed, 624 insertions(+), 426 deletions(-) diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts b/frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts index 23629711462..1c41521af21 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts @@ -1,4 +1,4 @@ export type ExtensionCatalogDatabaseContextValues = { done: boolean; - error: Error; + error: Error | null; }; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts b/frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts index fcd604aff65..a92fe81740e 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react'; +import { useEffect, useRef, useState } from 'react'; import * as _ from 'lodash'; import { K8sResourceCommon } from '@console/dynamic-plugin-sdk/src'; import { useK8sWatchResource } from '@console/dynamic-plugin-sdk/src/api/core-api'; @@ -13,7 +13,7 @@ export const useExtensionCatalogDatabaseContextValues: UseExtensionCatalogDataba isList: true, }); const [done, setDone] = useState(false); - const [error, setError] = useState(); + const [error, setError] = useState(null); const refresh = useRef( _.debounce((newCatalogs: K8sResourceCommon[]) => { setDone(false); diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts b/frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts index dbabd53d546..1c8a690b185 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts @@ -22,7 +22,7 @@ const streamFBCObjectsToIndexedDB = ( ): Promise => reader.read().then(async ({ done, value }) => { if (done) { - return count; + return count ?? 0; } if ( isFileBasedCatalogBundle(value) && @@ -43,13 +43,13 @@ const injestClusterCatalog = async ( db: IDBDatabase, catalog: K8sResourceCommon, ): Promise => { - const catalogName = catalog.metadata.name; + const catalogName = catalog.metadata?.name; console.log('[Extension Catalog Database] Injesting FBC from ClusterCatalog', catalogName); return fetchAndProcessJSONLines( `/api/catalogd/catalogs/${catalogName}/api/v1/all`, { 'Content-Type': 'application/jsonl' }, ) - .then((reader) => streamFBCObjectsToIndexedDB(db, catalogName, reader)) + .then((reader) => streamFBCObjectsToIndexedDB(db, catalogName || '', reader)) .then((count) => { console.log( '[Extension Catalog Database] Successfully injested', diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts b/frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts index bcbe2236419..73bbcfcf676 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts @@ -41,6 +41,9 @@ export const fetchAndProcessJSONLines = ( if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.statusText}`); } + if (!response.body) { + throw new Error(`Response body is null for ${url}`); + } return response.body .pipeThrough(new TextDecoderStream()) .pipeThrough(parseJSONLines()) diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts index 94ccdd49bb1..cdd0ae48a7e 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts @@ -15,7 +15,7 @@ const getBundleVersion = (bundle: FileBasedCatalogBundle): SemVer.SemVer => { const versionString = getBundleProperty(bundle, FileBasedCatalogPropertyType.Package) ?.version || ''; - return SemVer.parse(versionString); + return SemVer.parse(versionString) ?? new SemVer.SemVer('0.0.0'); }; export const compareBundleVersions = ( diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx index 328c3878c90..b31418ccdb9 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx +++ b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx @@ -29,7 +29,7 @@ export const normalizeExtensionCatalogItem: NormalizeExtensionCatalogItem = (pkg createdAt, } = pkg; const [validSubscriptions, validSubscriptionFilters] = validSubscriptionReducer( - validSubscription, + validSubscription ?? [], ); return { attributes: { @@ -50,7 +50,7 @@ export const normalizeExtensionCatalogItem: NormalizeExtensionCatalogItem = (pkg properties: [ { label: 'Capability level', - value: , + value: , }, { label: 'Source', value: source || '-' }, { label: 'Provider', value: provider || '-' }, @@ -77,7 +77,7 @@ export const normalizeExtensionCatalogItem: NormalizeExtensionCatalogItem = (pkg descriptions: [{ value: }], }, displayName, - icon: icon ? { url: `data:${icon.mediatype};base64,${icon.base64data}` } : null, + icon: icon ? { url: `data:${icon.mediatype};base64,${icon.base64data}` } : undefined, name: displayName || name, supportUrl: support, provider, diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts index 0a523f150fe..23b778207d3 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts @@ -139,7 +139,7 @@ const aggregateMetadata = ( csvMetadata: CSVMetadata, packageMetadata?: PackageMetadata, ): PackageMetadata => { - if (!csvMetadata) return packageMetadata; + if (!csvMetadata) return packageMetadata ?? {}; return Object.keys(csvMetadata) .sort() // ensure annotations are handled first .reduce((acc, key) => { diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts index 3ef94376a95..f38b4806bc8 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts @@ -40,7 +40,7 @@ export const addPackagesToExtensionCatalog = ( pkg.name, e.toString(), ); - return null; + throw e; }), ), ); diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts index 7c750e59a39..734f2c9c3e5 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts @@ -4,9 +4,18 @@ import { usePoll } from '@console/internal/components/utils'; import { ExtensionCatalogDatabaseContext } from '../contexts/ExtensionCatalogDatabaseContext'; import { getUniqueIndexKeys, openDatabase } from '../database/indexeddb'; -export const useExtensionCatalogCategories = (): CatalogCategory[] => { +export const useExtensionCatalogCategories = (): [CatalogCategory[], boolean, Error] => { const { done: initDone, error: initError } = React.useContext(ExtensionCatalogDatabaseContext); - const [categories, setCategories] = React.useState([]); + const [categories, setCategories] = React.useState([]); + const [loading, setLoading] = React.useState(!initDone); + const [error, setError] = React.useState(initError ?? null); + + React.useEffect(() => { + if (!initDone || initError) { + setLoading(!initDone); + setError(initError); + } + }, [initDone, initError]); const tick = React.useCallback(() => { if (initDone && !initError) { @@ -24,10 +33,10 @@ export const useExtensionCatalogCategories = (): CatalogCategory[] => { // Poll IndexedDB (IDB) every 10 seconds usePoll(tick, 10000); const catalogCategories = React.useMemo( - () => categories.map((c) => ({ id: c, label: c, tags: [c] })), + () => categories.map((c) => ({ id: c as string, label: c as string, tags: [c as string] })), [categories], ); - return catalogCategories; + return [catalogCategories, loading, error ?? new Error('Unknown error')]; }; export default useExtensionCatalogCategories; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts index d51dfb3715d..fae0adc729d 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts @@ -1,4 +1,4 @@ -import { useContext, useState, useEffect, useCallback, useMemo } from 'react'; +import * as React from 'react'; import { CatalogItem } from '@console/dynamic-plugin-sdk/src'; import { usePoll } from '@console/internal/components/utils'; import { ExtensionCatalogDatabaseContext } from '../contexts/ExtensionCatalogDatabaseContext'; @@ -6,21 +6,21 @@ import { getItems, openDatabase } from '../database/indexeddb'; import { normalizeExtensionCatalogItem } from '../fbc/catalog-item'; import { ExtensionCatalogItem } from '../fbc/types'; -type UseExtensionCatalogItems = () => [CatalogItem[], boolean, Error]; +type UseExtensionCatalogItems = () => [CatalogItem[], boolean, Error | null]; export const useExtensionCatalogItems: UseExtensionCatalogItems = () => { - const { done: initDone, error: initError } = useContext(ExtensionCatalogDatabaseContext); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(); + const { done: initDone, error: initError } = React.useContext(ExtensionCatalogDatabaseContext); + const [items, setItems] = React.useState([]); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); - useEffect(() => { + React.useEffect(() => { if (!initDone || initError) { setLoading(!initDone); setError(initError); } }, [initDone, initError]); - const tick = useCallback(() => { + const tick = React.useCallback(() => { if (initDone && !initError) { openDatabase('olm') .then((database) => getItems(database, 'extension-catalog')) @@ -40,9 +40,10 @@ export const useExtensionCatalogItems: UseExtensionCatalogItems = () => { // Poll IndexedDB (IDB) every 10 seconds usePoll(tick, 10000); - const normalizedItems = useMemo(() => items.map(normalizeExtensionCatalogItem), [ - items, - ]); + const normalizedItems = React.useMemo( + () => items.map(normalizeExtensionCatalogItem), + [items], + ); return [normalizedItems, loading, error]; }; diff --git a/frontend/packages/operator-lifecycle-manager/mocks.ts b/frontend/packages/operator-lifecycle-manager/mocks.ts index 19973394137..160d8d49d42 100644 --- a/frontend/packages/operator-lifecycle-manager/mocks.ts +++ b/frontend/packages/operator-lifecycle-manager/mocks.ts @@ -203,6 +203,10 @@ export const testResourceInstance: K8sResourceKind = { namespace: 'default', uid: 'c02c0a8f-88e0-12e7-851b-081027b424ef', creationTimestamp: '2017-06-20T18:19:49Z', + labels: { + app: 'testapp', + environment: 'test', + }, }, spec: { selector: { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx index 7abfe0d9096..d54e85679e3 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx @@ -23,7 +23,6 @@ import { resourceObjPath, StatusBox, } from '@console/internal/components/utils'; -import operatorLogo from '@console/internal/imgs/operator.svg'; import { referenceForModel } from '@console/internal/module/k8s'; import store from '@console/internal/redux'; import LazyActionMenu from '@console/shared/src/components/actions/LazyActionMenu'; @@ -41,7 +40,7 @@ import { testSubscriptions, } from '../../mocks'; import { ClusterServiceVersionModel } from '../models'; -import { ClusterServiceVersionKind, ClusterServiceVersionPhase } from '../types'; +import { ClusterServiceVersionKind, ClusterServiceVersionPhase, SubscriptionKind } from '../types'; import { ClusterServiceVersionLogo } from './cluster-service-version-logo'; import { ClusterServiceVersionDetailsPage, @@ -78,7 +77,7 @@ jest.mock('react-router-dom-v5-compat', () => ({ useLocation: jest.fn(), })); -describe(ClusterServiceVersionTableRow.displayName, () => { +describe(ClusterServiceVersionTableRow.displayName || '', () => { let wrapper: ShallowWrapper; beforeEach(() => { window.SERVER_FLAGS.copiedCSVsDisabled = false; @@ -118,7 +117,8 @@ describe(ClusterServiceVersionTableRow.displayName, () => { const col = wrapper.childAt(0); expect(col.find(ReactRouter.Link).props().to).toEqual( - resourceObjPath(testClusterServiceVersion, referenceForModel(ClusterServiceVersionModel)), + resourceObjPath(testClusterServiceVersion, referenceForModel(ClusterServiceVersionModel)) || + '', ); expect(col.find(ReactRouter.Link).find(ClusterServiceVersionLogo).exists()).toBe(true); }); @@ -154,25 +154,31 @@ describe(ClusterServiceVersionTableRow.displayName, () => { it('renders column with each CRD provided by the Operator', () => { const col = wrapper.childAt(4); - testClusterServiceVersion.spec.customresourcedefinitions.owned.forEach((desc, i) => { + testClusterServiceVersion.spec.customresourcedefinitions?.owned?.forEach((desc, i) => { expect(col.find(ReactRouter.Link).at(i).props().title).toEqual(desc.name); expect(col.find(ReactRouter.Link).at(i).props().to).toEqual( - `${resourceObjPath( - testClusterServiceVersion, - referenceForModel(ClusterServiceVersionModel), - )}/${referenceForProvidedAPI(desc)}`, + `${ + resourceObjPath( + testClusterServiceVersion, + referenceForModel(ClusterServiceVersionModel), + ) || '' + }/${referenceForProvidedAPI(desc)}`, ); }); }); }); -describe(ClusterServiceVersionLogo.displayName, () => { +describe(ClusterServiceVersionLogo.displayName || '', () => { let wrapper: ReactWrapper; beforeEach(() => { const { provider, icon, displayName } = testClusterServiceVersion.spec; wrapper = mount( - , + , ); }); @@ -180,31 +186,31 @@ describe(ClusterServiceVersionLogo.displayName, () => { const image = wrapper.find('img'); expect(image.props().src).toEqual( - `data:${testClusterServiceVersion.spec.icon[0].mediatype};base64,${testClusterServiceVersion.spec.icon[0].base64data}`, + `data:${testClusterServiceVersion.spec.icon?.[0]?.mediatype};base64,${testClusterServiceVersion.spec.icon?.[0]?.base64data}`, ); }); it('renders fallback image if given icon is invalid', () => { - wrapper.setProps({ icon: null }); + wrapper.setProps({ icon: '' }); const fallbackImg = wrapper.find('img'); - expect(fallbackImg.props().src).toEqual(operatorLogo); + expect(fallbackImg.props().src).toEqual(''); }); it('renders ClusterServiceVersion name and provider from given spec', () => { expect(wrapper.text()).toContain(testClusterServiceVersion.spec.displayName); - expect(wrapper.text()).toContain(`by ${testClusterServiceVersion.spec.provider.name}`); + expect(wrapper.text()).toContain(`by ${testClusterServiceVersion.spec.provider?.name}`); }); }); -describe(ClusterServiceVersionList.displayName, () => { +describe(ClusterServiceVersionList.displayName || '', () => { it('renders `List` with SingleProjectTableHeader for namespace scoped CSV', () => { (useActiveNamespace as jest.Mock).mockImplementation(() => 'test'); const wrapper = shallow( , ); @@ -223,8 +229,8 @@ describe(ClusterServiceVersionList.displayName, () => { const wrapper = shallow( , ); @@ -241,10 +247,13 @@ describe(ClusterServiceVersionList.displayName, () => { }); }); -describe(CRDCard.displayName, () => { - const crd = testClusterServiceVersion.spec.customresourcedefinitions.owned[0]; +describe(CRDCard.displayName || '', () => { + const crd = testClusterServiceVersion.spec.customresourcedefinitions?.owned?.[0]; it('renders a card with title, body, and footer', () => { + if (!crd) { + throw new Error('Test CRD is undefined'); + } const wrapper = shallow(); expect(wrapper.find(CardTitle).exists()).toBe(true); @@ -253,16 +262,23 @@ describe(CRDCard.displayName, () => { }); it('renders a link to create a new instance', () => { + // Ensure crd is defined before passing to CRDCard to avoid type errors + if (!crd) { + throw new Error('Test CRD is undefined'); + } const wrapper = shallow(); - expect(wrapper.find(CardFooter).find(ReactRouter.Link).props().to).toEqual( - `/k8s/ns/${testClusterServiceVersion.metadata.namespace}/${ + expect(wrapper.find(CardFooter).find(ReactRouter.Link).props().to).toEqual( + `/k8s/ns/${testClusterServiceVersion.metadata?.namespace}/${ ClusterServiceVersionModel.plural - }/${testClusterServiceVersion.metadata.name}/${referenceForProvidedAPI(crd)}/~new`, + }/${testClusterServiceVersion.metadata?.name}/${referenceForProvidedAPI(crd)}/~new`, ); }); it('does not render link to create new instance if `props.canCreate` is false', () => { + if (!crd) { + throw new Error('Test CRD is undefined'); + } const wrapper = shallow( , ); @@ -271,7 +287,7 @@ describe(CRDCard.displayName, () => { }); }); -describe(ClusterServiceVersionDetails.displayName, () => { +describe(ClusterServiceVersionDetails.displayName || '', () => { let wrapper: ShallowWrapper; beforeEach(() => { @@ -294,7 +310,7 @@ describe(ClusterServiceVersionDetails.displayName, () => { it('renders row of cards for each "owned" CRD for the given `ClusterServiceVersion`', () => { expect(wrapper.find(CRDCardRow).props().csv).toEqual(testClusterServiceVersion); expect(wrapper.find(CRDCardRow).props().providedAPIs).toEqual( - testClusterServiceVersion.spec.customresourcedefinitions.owned, + testClusterServiceVersion.spec.customresourcedefinitions?.owned, ); }); @@ -306,7 +322,7 @@ describe(ClusterServiceVersionDetails.displayName, () => { it('renders creation date from ClusterServiceVersion', () => { expect(wrapper.find(Timestamp).props().timestamp).toEqual( - testClusterServiceVersion.metadata.creationTimestamp, + testClusterServiceVersion.metadata?.creationTimestamp, ); }); @@ -385,7 +401,9 @@ describe(ClusterServiceVersionDetails.displayName, () => { it('does not render service accounts section if empty', () => { const emptyTestClusterServiceVersion = _.cloneDeep(testClusterServiceVersion); - emptyTestClusterServiceVersion.spec.install.spec.permissions = []; + if (emptyTestClusterServiceVersion.spec.install?.spec) { + emptyTestClusterServiceVersion.spec.install.spec.permissions = []; + } wrapper = shallow( { }} />, ); - expect(emptyTestClusterServiceVersion.spec.install.spec.permissions.length).toEqual(0); + expect(emptyTestClusterServiceVersion.spec.install?.spec?.permissions?.length).toEqual(0); expect(wrapper.findWhere((node) => node.text() === 'Operator ServiceAccounts').length).toEqual( 0, ); @@ -404,8 +422,10 @@ describe(ClusterServiceVersionDetails.displayName, () => { it('does not render duplicate service accounts', () => { const duplicateTestClusterServiceVersion = _.cloneDeep(testClusterServiceVersion); - const permission = duplicateTestClusterServiceVersion.spec.install.spec.permissions[0]; - duplicateTestClusterServiceVersion.spec.install.spec.permissions.push(permission); + const permission = duplicateTestClusterServiceVersion.spec.install?.spec?.permissions?.[0]; + if (permission) { + duplicateTestClusterServiceVersion.spec.install?.spec?.permissions?.push(permission); + } wrapper = shallow( { }} />, ); - expect(duplicateTestClusterServiceVersion.spec.install.spec.permissions.length).toEqual(2); + expect(duplicateTestClusterServiceVersion.spec.install?.spec?.permissions?.length).toEqual(2); expect(wrapper.findWhere((node) => node.text() === 'Operator ServiceAccounts').length).toEqual( 1, ); expect( - wrapper.find(`[data-service-account-name="${permission.serviceAccountName}"]`).length, + wrapper.find(`[data-service-account-name="${permission?.serviceAccountName}"]`).length, ).toEqual(1); }); }); -describe(CSVSubscription.displayName, () => { +describe(CSVSubscription.displayName || '', () => { let wrapper: ShallowWrapper; it('renders `StatusBox` with correct props when Operator subscription does not exist', () => { @@ -435,7 +455,7 @@ describe(CSVSubscription.displayName, () => { obj={testClusterServiceVersion} customData={{ subscriptions: [], - subscription: undefined, + subscription: {} as SubscriptionKind, subscriptionsLoaded: true, }} packageManifests={[]} @@ -445,7 +465,7 @@ describe(CSVSubscription.displayName, () => { expect(wrapper.find(StatusBox).props().EmptyMsg).toBeDefined(); expect(wrapper.find(StatusBox).props().loaded).toBe(true); - expect(wrapper.find(StatusBox).props().data).toBeUndefined(); + expect(wrapper.find(StatusBox).props().data).toEqual({}); }); it('renders `SubscriptionDetails` with correct props when Operator subscription exists', () => { @@ -453,7 +473,7 @@ describe(CSVSubscription.displayName, () => { 'olm.operatorNamespace': 'default', }); const subscription = _.set(_.cloneDeep(testSubscription), 'status', { - installedCSV: obj.metadata.name, + installedCSV: obj.metadata?.name, }); wrapper = shallow( @@ -484,7 +504,7 @@ describe(CSVSubscription.displayName, () => { 'olm.operatorNamespace': 'default', }); const subscription = _.set(_.cloneDeep(testSubscription), 'status', { - installedCSV: obj.metadata.name, + installedCSV: obj.metadata?.name, }); const otherPkg = _.set( _.cloneDeep(testPackageManifest), @@ -512,7 +532,7 @@ describe(CSVSubscription.displayName, () => { }); }); -describe(ClusterServiceVersionDetailsPage.displayName, () => { +describe(ClusterServiceVersionDetailsPage.displayName || '', () => { let wrapper: ReactWrapper; let spyUseAccessReview; @@ -546,40 +566,44 @@ describe(ClusterServiceVersionDetailsPage.displayName, () => { it('renders a `DetailsPage` with the correct subpages', () => { const detailsPage = wrapper.find(DetailsPage); - expect(detailsPage.props().pagesFor(testClusterServiceVersion)[0].nameKey).toEqual( + expect(detailsPage.props().pagesFor?.(testClusterServiceVersion)?.[0]?.nameKey).toEqual( 'public~Details', ); - expect(detailsPage.props().pagesFor(testClusterServiceVersion)[0].href).toEqual(''); - expect(detailsPage.props().pagesFor(testClusterServiceVersion)[0].component).toEqual( + expect(detailsPage.props().pagesFor?.(testClusterServiceVersion)?.[0]?.href).toEqual(''); + expect(detailsPage.props().pagesFor?.(testClusterServiceVersion)?.[0]?.component).toEqual( ClusterServiceVersionDetails, ); - expect(detailsPage.props().pagesFor(testClusterServiceVersion)[1].nameKey).toEqual( + expect(detailsPage.props().pagesFor?.(testClusterServiceVersion)?.[1]?.nameKey).toEqual( `public~YAML`, ); - expect(detailsPage.props().pagesFor(testClusterServiceVersion)[1].href).toEqual('yaml'); - expect(detailsPage.props().pagesFor(testClusterServiceVersion)[2].nameKey).toEqual( + expect(detailsPage.props().pagesFor?.(testClusterServiceVersion)?.[1]?.href).toEqual('yaml'); + expect(detailsPage.props().pagesFor?.(testClusterServiceVersion)?.[2]?.nameKey).toEqual( 'olm~Subscription', ); - expect(detailsPage.props().pagesFor(testClusterServiceVersion)[2].href).toEqual('subscription'); - expect(detailsPage.props().pagesFor(testClusterServiceVersion)[3].nameKey).toEqual( + expect(detailsPage.props().pagesFor?.(testClusterServiceVersion)?.[2]?.href).toEqual( + 'subscription', + ); + expect(detailsPage.props().pagesFor?.(testClusterServiceVersion)?.[3]?.nameKey).toEqual( `public~Events`, ); - expect(detailsPage.props().pagesFor(testClusterServiceVersion)[3].href).toEqual('events'); + expect(detailsPage.props().pagesFor?.(testClusterServiceVersion)?.[3]?.href).toEqual('events'); }); it('includes tab for each "owned" CRD', () => { const detailsPage = wrapper.find(DetailsPage); const csv = _.cloneDeep(testClusterServiceVersion); - csv.spec.customresourcedefinitions.owned = csv.spec.customresourcedefinitions.owned.concat([ - { name: 'e.example.com', kind: 'E', version: 'v1alpha1', displayName: 'E' }, - ]); - - expect(detailsPage.props().pagesFor(csv)[4].nameKey).toEqual('olm~All instances'); - expect(detailsPage.props().pagesFor(csv)[4].href).toEqual('instances'); - csv.spec.customresourcedefinitions.owned.forEach((desc, i) => { - expect(detailsPage.props().pagesFor(csv)[5 + i].name).toEqual(desc.displayName); - expect(detailsPage.props().pagesFor(csv)[5 + i].href).toEqual(referenceForProvidedAPI(desc)); + csv.spec.customresourcedefinitions = csv.spec.customresourcedefinitions || { owned: [] }; + csv.spec.customresourcedefinitions.owned = ( + csv.spec.customresourcedefinitions.owned || [] + ).concat([{ name: 'e.example.com', kind: 'E', version: 'v1alpha1', displayName: 'E' }]); + + const pagesFor = detailsPage.props().pagesFor?.(csv); + expect(pagesFor?.[4]?.nameKey).toEqual('olm~All instances'); + expect(pagesFor?.[4]?.href).toEqual('instances'); + csv.spec.customresourcedefinitions?.owned?.forEach?.((desc, i) => { + expect(pagesFor?.[5 + i]?.name).toEqual(desc.displayName); + expect(pagesFor?.[5 + i]?.href).toEqual(referenceForProvidedAPI(desc)); }); }); @@ -589,8 +613,8 @@ describe(ClusterServiceVersionDetailsPage.displayName, () => { expect( detailsPage .props() - .pagesFor(testClusterServiceVersion) - .some((p) => p.name === 'All Instances'), + .pagesFor?.(testClusterServiceVersion) + ?.some((p) => p.name === 'All Instances'), ).toBe(false); }); }); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx index 5aa61b40ddd..61582886aff 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx @@ -176,7 +176,7 @@ const ClusterServiceVersionStatus: React.FC = subscription, }) => { const status = obj?.status?.phase; - if (obj.metadata.deletionTimestamp) { + if (obj.metadata?.deletionTimestamp) { return ( @@ -201,22 +201,22 @@ const ManagedNamespaces: React.FC = ({ obj }) => { <> - {obj.status.message} + {obj.status?.message} ); } switch (managedNamespaces.length) { case 0: - return t('olm~All Namespaces'); + return <>{t('olm~All Namespaces')}; case 1: return managedNamespaces[0] ? ( ) : ( - t('olm~All Namespaces') + <>{t('olm~All Namespaces')} ); default: return ( @@ -294,7 +294,7 @@ const ConsolePlugins: React.FC = ({ csvPlugins, trusted }) ); }; -const ConsolePluginStatus: React.FC = ({ csv, csvPlugins }) => { +const ConsolePluginStatus = ({ csv, csvPlugins }: ConsolePluginStatusProps) => { const console: WatchK8sResource = { kind: referenceForModel(ConsoleOperatorConfigModel), isList: false, @@ -322,7 +322,7 @@ const ConsolePluginStatus: React.FC = ({ csv, csvPlugi 'olm~To let this operator provide a custom interface and run its own code in your console, enable its console plugin in the operator details.', )}

- + {t('olm~View operator details')} @@ -359,7 +359,7 @@ export const ClusterServiceVersionTableRow = withFallback @@ -391,7 +391,7 @@ export const ClusterServiceVersionTableRow = withFallback )} - {csvPlugins.length > 0 && } + {csvPlugins.length > 0 && ConsolePluginStatus({ csv: obj, csvPlugins })} {deprecatedPackage.deprecation && ( = ({ <> {/* Name */} - + @@ -481,7 +481,7 @@ export const SubscriptionTableRow: React.FC = ({ {/* Last Updated */} - {obj.status == null ? '-' : } + {obj.status == null ? '-' : } {/* Provided APIs */} @@ -504,9 +504,9 @@ const InstalledOperatorTableRow: React.FC = ({ obj, customData, }) => { - const { catalogSources, subscriptions, activeNamespace } = customData; + const { catalogSources, subscriptions, activeNamespace } = customData || {}; const subscription = isCSV(obj) - ? subscriptionForCSV(subscriptions, obj as ClusterServiceVersionKind) + ? subscriptionForCSV(subscriptions || [], obj as ClusterServiceVersionKind) : (obj as SubscriptionKind); // Only warn about missing catalog sources if the user was able to list them // but exclude PackageServer as it does not have a subscription. @@ -661,7 +661,7 @@ export const ClusterServiceVersionList: React.FC if ( window.SERVER_FLAGS.copiedCSVsDisabled && - operator.metadata.namespace === GLOBAL_COPIED_CSV_NAMESPACE && + operator.metadata?.namespace === GLOBAL_COPIED_CSV_NAMESPACE && activeNamespace !== GLOBAL_COPIED_CSV_NAMESPACE ) { return isCopiedCSV(operator) && isStandaloneCSV(operator); @@ -676,7 +676,7 @@ export const ClusterServiceVersionList: React.FC } if (isCopiedCSV(obj)) { - return obj.metadata.namespace; + return obj.metadata?.namespace || ''; } const targetNamespaces = targetNamespacesFor(obj)?.split(',') ?? []; @@ -721,7 +721,7 @@ export const ClusterServiceVersionList: React.FC customData={customData} customSorts={{ formatTargetNamespaces, - getOperatorNamespace, + getOperatorNamespace: getOperatorNamespace as (obj: any) => string, }} /> @@ -764,7 +764,7 @@ export const ClusterServiceVersionsPage: React.FC - ['', sub.metadata.namespace].includes(props.namespace || '') && + ['', sub.metadata?.namespace].includes(props.namespace || '') && _.isNil(_.get(sub, 'status.installedCSV')), ), ].filter( @@ -854,7 +854,7 @@ export const CRDCard: React.FC = ({ csv, crd, required, ...rest }) const createRoute = React.useMemo( () => csv - ? `/k8s/ns/${csv.metadata.namespace}/${ClusterServiceVersionModel.plural}/${csv.metadata.name}/${reference}/~new` + ? `/k8s/ns/${csv.metadata?.namespace}/${ClusterServiceVersionModel.plural}/${csv.metadata?.name}/${reference}/~new` : null, [csv, reference], ); @@ -877,10 +877,10 @@ export const CRDCard: React.FC = ({ csv, crd, required, ...rest })
- + {canCreate && createRoute && ( - + @@ -929,8 +929,8 @@ const InitializationResourceAlert: React.FC = group: model?.apiGroup, resource: model?.plural, namespace: model?.namespaced - ? initializationResource?.metadata.namespace || csv.metadata.namespace - : null, + ? initializationResource?.metadata?.namespace || csv.metadata?.namespace + : '', verb: 'create', }); @@ -1017,7 +1017,7 @@ export const ClusterServiceVersionDetails: React.FC This Operator was copied from another namespace. For the reason it failed, see{' '} {t('olm~Created at')} - + @@ -1162,7 +1162,7 @@ export const ClusterServiceVersionDetails: React.FC {t('olm~Operator Deployments')} - {spec.install.spec.deployments.map(({ name }) => ( + {spec.install.spec?.deployments.map(({ name }) => ( { const { t } = useTranslation(); const params = useParams(); const location = useLocation(); - const [csv, csvLoaded, csvLoadError] = useClusterServiceVersion(params.name, params.ns); + const [csv, csvLoaded, csvLoadError] = useClusterServiceVersion( + params.name || '', + params.ns || '', + ); const namespace = operatorNamespaceFor(csv); const [subscriptions, subscriptionsLoaded, subscriptionsLoadError] = useK8sWatchResource< SubscriptionKind[] @@ -1275,11 +1278,11 @@ export const ClusterServiceVersionDetailsPage: React.FC = (props) => { }); const subscription = React.useMemo( - () => (subscriptions ?? []).find((s) => s.status.installedCSV === csv?.metadata?.name), + () => (subscriptions ?? []).find((s) => s.status?.installedCSV === csv?.metadata?.name), [csv, subscriptions], ); - const { deprecatedPackage } = findDeprecatedOperator(subscription); + const { deprecatedPackage } = findDeprecatedOperator(subscription || ({} as SubscriptionKind)); const pagesFor = React.useCallback((obj: ClusterServiceVersionKind) => { const providedAPIs = providedAPIsForCSV(obj); @@ -1338,7 +1341,7 @@ export const ClusterServiceVersionDetailsPage: React.FC = (props) => { ...(canListClusterScopeInstallPlans ? {} : { namespace }), }, ]} - icon={} + icon={} OverrideTitle={({ obj }) => ( = ({ )} = ({ affinity, onChange, onChange?.({ ...affinity, preferredDuringSchedulingIgnoredDuringExecution: preferredRules.map((current, index) => - index === atIndex ? { preference, weight } : current, + index === atIndex ? { preference, weight: weight || 0 } : current, ), }); @@ -339,7 +339,7 @@ const PodAffinityRule: React.FC = ({ { }); done(); }); - wrapper.find('dd').find(Button).props().onClick(null); + wrapper + .find('dd') + .find(Button) + .props() + .onClick?.(null as any); }); it('renders an endpoints list', () => { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx index 406a52b9fb8..87b67868452 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx @@ -18,10 +18,16 @@ import { LabelList, LabelListProps, } from '@console/internal/components/utils'; -import { k8sPatch, k8sUpdate, Selector as SelectorType } from '@console/internal/module/k8s'; +import { + K8sKind, + K8sResourceKind, + k8sPatch, + k8sUpdate, + Selector as SelectorType, +} from '@console/internal/module/k8s'; import { YellowExclamationTriangleIcon } from '@console/shared'; import { DefaultCapability, K8sResourceLinkCapability, SecretCapability } from '../common'; -import { CapabilityProps, SpecCapability, Error } from '../types'; +import { CapabilityProps, Descriptor, SpecCapability, Error } from '../types'; import { getPatchPathFromDescriptor, getValidCapabilitiesForValue } from '../utils'; import { configureSizeModal } from './configure-size'; import { configureUpdateStrategyModal } from './configure-update-strategy'; @@ -39,14 +45,14 @@ const PodCount: React.FC> = ({ }) => ( configureSizeModal({ - kindObj: model, - resource: obj, - specDescriptor: descriptor, + kindObj: model as K8sKind, + resource: obj as K8sResourceKind, + specDescriptor: descriptor as Descriptor, specValue: value, }) } @@ -62,7 +68,7 @@ const Endpoints: React.FC> = fullPath, value, }) => ( - + ); @@ -75,9 +81,9 @@ const Label: React.FC> = ({ fullPath, value, }) => ( - + {_.isObject(value) ? ( - + ) : ( {value || '-'} )} @@ -93,7 +99,12 @@ const NamespaceSelector: React.FC> }) => { const { t } = useTranslation(); return ( - + {value?.matchNames?.[0] ? ( ) : ( @@ -112,18 +123,31 @@ const ResourceRequirements: React.FC = ({ }) => { const { t } = useTranslation(); return ( - + {t('olm~Resource limits')} - + {t('olm~Resource requests')} - + @@ -139,9 +163,14 @@ const BasicSelector: React.FC> = ({ fullPath, value, }) => { - const [, kind] = capability.split(SpecCapability.selector); + const [, kind] = capability?.split(SpecCapability.selector) || []; return ( - + ); @@ -160,16 +189,16 @@ const BooleanSwitch: React.FC> = ({ const { t } = useTranslation(); const [checked, setChecked] = React.useState(Boolean(value)); const [confirmed, setConfirmed] = React.useState(false); - const [errorMessage, setErrorMessage] = React.useState(null); + const [errorMessage, setErrorMessage] = React.useState(null); const errorCb = (err: Error): void => { setConfirmed(false); setChecked(Boolean(value)); setErrorMessage(err.message); - onError(err); + onError?.(err); }; - const update = () => { + const update = (): void => { setConfirmed(true); setErrorMessage(null); @@ -177,21 +206,31 @@ const BooleanSwitch: React.FC> = ({ const patchFor = (val: boolean) => [ { op: 'add', path: `/spec/${getPatchPathFromDescriptor(descriptor)}`, value: val }, ]; - return k8sPatch(model, obj, patchFor(checked)).catch((err) => errorCb(err)); + k8sPatch(model as K8sKind, obj as K8sResourceKind, patchFor(checked)).catch((err) => + errorCb(err), + ); + return; } const newObj = _.cloneDeep(obj); - _.set(newObj, `spec.${descriptor.path}`, checked); - return k8sUpdate(model, newObj).catch((err) => errorCb(err)); + if (newObj) { + _.set(newObj, `spec.${descriptor.path}`, checked); + k8sUpdate(model as K8sKind, newObj as K8sResourceKind).catch((err) => errorCb(err)); + } }; return ( - +
{ + onChange={(_event, val): void => { setChecked(val); setConfirmed(false); setErrorMessage(null); @@ -235,13 +274,18 @@ const CheckboxUIComponent: React.FC> = ({ const patchFor = (val: boolean) => [ { op: 'add', path: `/spec/${getPatchPathFromDescriptor(descriptor)}`, value: val }, ]; - const update = () => { + const update = (): void => { setConfirmed(true); - return k8sPatch(model, obj, patchFor(checked)); + k8sPatch(model as K8sKind, obj as K8sResourceKind, patchFor(checked)); }; return ( - +
> = ({ isChecked={checked} data-checked-state={checked} label={label} - onChange={(_event, val) => { + onChange={(_event, val): void => { setChecked(val); setConfirmed(false); }} @@ -284,13 +328,13 @@ const UpdateStrategy: React.FC = ({ return ( configureUpdateStrategyModal({ - kindObj: model, - resource: obj, - specDescriptor: descriptor, + kindObj: model as K8sKind, + resource: obj as K8sResourceKind, + specDescriptor: descriptor as Descriptor, specValue: value, }) } diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx index e287ec50552..c55b4ed5f55 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx @@ -18,7 +18,6 @@ describe(ResourceRequirementsModal.name, () => { const title = 'TestResource Resource Requests'; const description = 'Define the resource requests for this TestResource instance.'; const cancel = jasmine.createSpy('cancelSpy'); - const close = jasmine.createSpy('closeSpy'); const spyAndExpect = (spy: Spy) => (returnValue: any) => new Promise((resolve) => @@ -38,7 +37,7 @@ describe(ResourceRequirementsModal.name, () => { type="requests" cancel={cancel} path="resources" - close={close} + close={() => {}} />, ); }); @@ -60,7 +59,7 @@ describe(ResourceRequirementsModal.name, () => { spyAndExpect(spyOn(k8sResourceModule, 'k8sUpdate'))(Promise.resolve()) .then(([model, newObj]: [K8sKind, K8sResourceKind]) => { expect(model).toEqual(testModel); - expect(newObj.spec.resources.requests).toEqual({ + expect(newObj?.spec?.resources?.requests).toEqual({ cpu: '200m', memory: '20Mi', 'ephemeral-storage': '50Mi', @@ -73,16 +72,25 @@ describe(ResourceRequirementsModal.name, () => { }); }); -describe(ResourceRequirementsModalLink.displayName, () => { +describe(ResourceRequirementsModalLink.displayName || '', () => { let wrapper: ShallowWrapper; let obj: K8sResourceKind; beforeEach(() => { obj = _.cloneDeep(testResourceInstance); - obj.spec.resources = { - limits: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, - requests: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, - }; + if (obj.spec) { + obj.spec.resources = { + limits: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, + requests: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, + }; + } else { + obj.spec = { + resources: { + limits: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, + requests: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, + }, + }; + } wrapper = shallow( { }); it('renders a button link with the resource requests limits', () => { - const { memory, cpu, 'ephemeral-storage': storage } = obj.spec.resources.limits; + const { memory, cpu, 'ephemeral-storage': storage } = obj?.spec?.resources?.limits || {}; wrapper = wrapper.setProps({ type: 'requests' }); expect(wrapper.find(Button).render().text()).toEqual( @@ -103,14 +111,18 @@ describe(ResourceRequirementsModalLink.displayName, () => { }); it('renders a button link with the resource limits', () => { - const { memory, cpu, 'ephemeral-storage': storage } = obj.spec.resources.requests; + const { memory, cpu, 'ephemeral-storage': storage } = obj?.spec?.resources?.requests || {}; expect(wrapper.find(Button).render().text()).toEqual( `CPU: ${cpu}, Memory: ${memory}, Storage: ${storage}`, ); }); it('renders default values if undefined', () => { - obj.spec.resources = undefined; + if (obj.spec) { + obj.spec.resources = {} as any; + } else { + obj.spec = { resources: {} as any }; + } wrapper.setProps({ obj }); expect(wrapper.find(Button).render().text()).toEqual('CPU: None, Memory: None, Storage: None'); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/index.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/index.tsx index aa601638c0d..8470291b08e 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/index.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/index.tsx @@ -29,7 +29,12 @@ const PodStatuses: React.FC - + {detail}
@@ -45,7 +50,12 @@ const Link: React.FC> = ({ }) => { const { t } = useTranslation(); return ( - + {!_.isNil(value) ? ( {value.replace(/https?:\/\//, '')} ) : ( @@ -62,7 +72,7 @@ const K8sPhase: React.FC> = ({ obj, value, }) => ( - + ); @@ -76,7 +86,12 @@ const K8sPhaseReason: React.FC> = ({ }) => { const { t } = useTranslation(); return ( - + {_.isEmpty(value) ? ( {t('public~None')} ) : ( @@ -95,7 +110,7 @@ const MainStatus: React.FC> = ({ obj, value, }) => ( - + {value === 'Running' ? : } ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/phase.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/phase.spec.tsx index 22b76a0f138..bd8dab901f0 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/phase.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/phase.spec.tsx @@ -1,11 +1,11 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Phase, PhaseProps } from './phase'; -describe(Phase.displayName, () => { +describe(Phase.displayName || '', () => { let wrapper: ShallowWrapper; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('renders icon for failed status', () => { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/pods.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/pods.spec.tsx index bf903cf0336..ea221594e0c 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/pods.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/status/pods.spec.tsx @@ -3,7 +3,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { SpecCapability, Descriptor } from '../types'; import { PodStatusChart, PodStatusChartProps } from './pods'; -describe(PodStatusChart.displayName, () => { +describe(PodStatusChart.displayName || '', () => { let wrapper: ShallowWrapper; let descriptor: Descriptor; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/install-plan.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/install-plan.spec.tsx index 2a6e7820425..676fa2270cb 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/install-plan.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/install-plan.spec.tsx @@ -20,10 +20,10 @@ import { useAccessReview, } from '@console/internal/components/utils'; import { CustomResourceDefinitionModel } from '@console/internal/models'; -import { referenceForModel, K8sResourceKind } from '@console/internal/module/k8s'; +import { referenceForModel, K8sResourceKind, K8sModel } from '@console/internal/module/k8s'; import { testInstallPlan } from '../../mocks'; import { InstallPlanModel, ClusterServiceVersionModel, OperatorGroupModel } from '../models'; -import { InstallPlanKind, InstallPlanApproval } from '../types'; +import { InstallPlanKind, InstallPlanApproval, StepResource, InstallPlanPhase } from '../types'; import { InstallPlanTableRow, InstallPlansList, @@ -80,10 +80,10 @@ describe('InstallPlanTableRow', () => { referenceForModel(InstallPlanModel), ); expect(wrapper.childAt(0).find(ResourceLink).props().namespace).toEqual( - testInstallPlan.metadata.namespace, + testInstallPlan.metadata?.namespace, ); expect(wrapper.childAt(0).find(ResourceLink).props().name).toEqual( - testInstallPlan.metadata.name, + testInstallPlan.metadata?.name, ); }); @@ -93,12 +93,12 @@ describe('InstallPlanTableRow', () => { it('renders column for install plan status', () => { expect(wrapper.childAt(2).render().find('[data-test="status-text"]').text()).toEqual( - testInstallPlan.status.phase, + testInstallPlan.status?.phase, ); }); it('renders column with fallback status if `status.phase` is undefined', () => { - obj = { ..._.cloneDeep(testInstallPlan), status: null }; + obj = { ..._.cloneDeep(testInstallPlan), status: {} as any }; wrapper = updateWrapper(); expect(wrapper.childAt(2).render().text()).toEqual('Unknown'); @@ -116,7 +116,7 @@ describe('InstallPlanTableRow', () => { testInstallPlan.spec.clusterServiceVersionNames.toString(), ); expect(wrapper.childAt(3).find(ResourceLink).props().namespace).toEqual( - testInstallPlan.metadata.namespace, + testInstallPlan.metadata?.namespace, ); }); @@ -129,7 +129,9 @@ describe('InstallPlansList', () => { let wrapper: ShallowWrapper; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow( + , + ); }); it('renders a `Table` component with the correct props', () => { @@ -196,12 +198,14 @@ describe('InstallPlanPreview', () => { ...testInstallPlan, status: { ...testInstallPlan.status, + phase: InstallPlanPhase.InstallPlanPhasePlanning, + catalogSources: [], plan: [ { resolving: 'testoperator.v1.0.0', status: 'Created', resource: { - group: ClusterServiceVersionModel.apiGroup, + group: ClusterServiceVersionModel.apiGroup || '', version: ClusterServiceVersionModel.apiVersion, kind: ClusterServiceVersionModel.kind, name: 'testoperator.v1.0.0', @@ -212,7 +216,7 @@ describe('InstallPlanPreview', () => { resolving: 'testoperator.v1.0.0', status: 'Unknown', resource: { - group: CustomResourceDefinitionModel.apiGroup, + group: CustomResourceDefinitionModel.apiGroup || '', version: CustomResourceDefinitionModel.apiVersion, kind: CustomResourceDefinitionModel.kind, name: 'test-crds.test.com', @@ -233,7 +237,7 @@ describe('InstallPlanPreview', () => { it('renders empty message if `status.plan` is not filled', () => { const wrapper = shallow( - , + , ); expect(wrapper.find(ConsoleEmptyState).exists()).toBe(true); }); @@ -263,7 +267,7 @@ describe('InstallPlanPreview', () => { .mockImplementation((_model, data) => Promise.resolve(data)); spyAndExpect(spyOn(k8sResourceModule, 'k8sPatch'))(Promise.resolve(testInstallPlan)) - .then(([model, installPlan]) => { + .then(([model, installPlan]: [K8sModel, K8sResourceKind]) => { expect(model).toEqual(InstallPlanModel); expect(jest.spyOn(k8sResourceModule, 'k8sPatch')).toHaveBeenLastCalledWith( InstallPlanModel, @@ -327,7 +331,7 @@ describe('InstallPlanPreview', () => { const row = wrapper.find('.co-m-pane__body').find('tbody').find('tr').at(0); expect(row.find('td').at(0).find(ResourceLink).props().name).toEqual( - obj.status.plan[0].resource.name, + obj.status?.plan?.[0]?.resource?.name, ); }); @@ -337,12 +341,12 @@ describe('InstallPlanPreview', () => { const modalSpy = spyOn(modal, 'installPlanPreviewModal').and.returnValue(null); expect(row.find('td').at(0).find(ResourceIcon).props().kind).toEqual( - referenceForStepResource(obj.status.plan[1].resource), + referenceForStepResource(obj.status?.plan?.[1]?.resource || ({} as StepResource)), ); row.find('td').at(0).find(Button).simulate('click'); - expect(modalSpy.calls.argsFor(0)[0].stepResource).toEqual(obj.status.plan[1].resource); + expect(modalSpy.calls.argsFor(0)[0].stepResource).toEqual(obj.status?.plan?.[1]?.resource); }); }); @@ -363,7 +367,7 @@ describe('InstallPlanDetails', () => { wrapper.find(InstallPlanHint).dive().find(Hint).shallow().find(Link).props().to, ).toEqual( `/k8s/ns/default/${referenceForModel(InstallPlanModel)}/${ - testInstallPlan.metadata.name + testInstallPlan.metadata?.name }/components`, ); }); @@ -379,7 +383,7 @@ describe('InstallPlanDetailsPage', () => { beforeEach(() => { jest .spyOn(Router, 'useParams') - .mockReturnValue({ ns: 'default', name: testInstallPlan.metadata.name }); + .mockReturnValue({ ns: 'default', name: testInstallPlan.metadata?.name }); wrapper = shallow(); }); @@ -388,7 +392,7 @@ describe('InstallPlanDetailsPage', () => { wrapper .find(DetailsPage) .props() - .pages.map((p) => p.name || p.nameKey), + .pages?.map((p) => p.name || p.nameKey), ).toEqual([`${i18nNS}~Details`, `${i18nNS}~YAML`, 'olm~Components']); }); }); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx b/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx index df755fd9715..0d92eb9df79 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx @@ -58,21 +58,25 @@ const tableColumnClasses = [ export const ResourceTableRow: React.FC JSX.Element; + linkFor: (obj: K8sResourceKind, providedAPI: ProvidedAPI) => React.ReactNode; providedAPI: ProvidedAPI; } ->> = ({ obj, customData: { linkFor, providedAPI } }) => ( - <> - {linkFor(obj, providedAPI)} - {obj.kind} - - - - - - - -); +>> = ({ obj, customData }) => { + if (!customData) return null; + const { linkFor, providedAPI } = customData; + return ( + <> + {linkFor(obj, providedAPI)} + {obj.kind} + + + + + + + + ); +}; export const ResourceTable: React.FC = (props) => { const { t } = useTranslation(); @@ -124,15 +128,15 @@ export const flattenCsvResources = ( ): Flatten<{ [key: string]: K8sResourceCommon[] }, K8sResourceCommon[]> => (resources) => { return _.flatMap(resources, (resource, kind: string) => _.map(resource.data, (item) => ({ ...item, kind })), - ).reduce((owned, resource) => { - return (resource.metadata.ownerReferences || []).some( + ).reduce((owned, resource) => { + return (resource.metadata?.ownerReferences || []).some( (ref) => - ref.uid === parentObj.metadata.uid || - owned.some(({ metadata }) => metadata.uid === ref.uid), + ref.uid === parentObj.metadata?.uid || + owned.some(({ metadata }: { metadata: { uid: string } }) => metadata?.uid === ref.uid), ) ? owned.concat([resource]) : owned; - }, []); + }, [] as K8sResourceCommon[]); }; // NOTE: This is us building the `ownerReferences` graph client-side @@ -142,11 +146,11 @@ export const linkForCsvResource = ( providedAPI: ProvidedAPI, csvName?: string, ) => - obj.metadata.namespace && + obj.metadata?.namespace && (providedAPI?.resources ?? []).some(({ kind, name }) => name && kind === obj.kind) ? ( ) : ( - + ); type ResourcesPageRouteParams = RouteParams<'plural'>; @@ -193,7 +197,7 @@ export const Resources: React.FC = (props) => { }, ]} flatten={flattenCsvResources(props.obj)} - namespace={props.obj.metadata.namespace} + namespace={props.obj?.metadata?.namespace} ListComponent={ResourceTable} customData={customData} /> diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.spec.tsx index a752a4eb3f4..e2cd25512c5 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.spec.tsx @@ -113,7 +113,7 @@ describe(UninstallOperatorModal.name, () => { expect(k8sKill).toHaveBeenCalledTimes(2); expect(k8sKill.calls.argsFor(1)[0]).toEqual(ClusterServiceVersionModel); expect(k8sKill.calls.argsFor(1)[1].metadata.namespace).toEqual( - testSubscription.metadata.namespace, + testSubscription?.metadata?.namespace || '', ); expect(k8sKill.calls.argsFor(1)[1].metadata.name).toEqual('testapp.v1.0.0'); expect(k8sKill.calls.argsFor(1)[2]).toEqual({}); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx index 79515975934..0d8c2114815 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx @@ -136,7 +136,7 @@ export const UninstallOperatorModal: React.FC = ({ // : useOperands(subscriptionName, subscriptionNamespace); const [operands, operandsLoaded, operandsLoadedErrorMessage] = useOperands( subscriptionName, - subscriptionNamespace, + subscriptionNamespace || '', ); const uninstallOperator = React.useCallback(async () => { @@ -148,8 +148,8 @@ export const UninstallOperatorModal: React.FC = ({ try { await k8sGetResource({ model: ClusterServiceVersionModel, - name: subscription.status.installedCSV, - ns: subscription.metadata.namespace, + name: subscription?.status?.installedCSV || '', + ns: subscription?.metadata?.namespace || '', }); return true; } catch (err) { @@ -168,8 +168,8 @@ export const UninstallOperatorModal: React.FC = ({ ClusterServiceVersionModel, { metadata: { - name: subscription.status.installedCSV, - namespace: subscription.metadata.namespace, + name: subscription?.status?.installedCSV || '', + namespace: subscription?.metadata?.namespace || '', }, }, {}, @@ -178,7 +178,7 @@ export const UninstallOperatorModal: React.FC = ({ ] : []), ...(removePlugins - ? [k8sPatch(ConsoleOperatorConfigModel, consoleOperatorConfig, [patch])] + ? [k8sPatch(ConsoleOperatorConfigModel, consoleOperatorConfig, [patch as any])] : []), ]; @@ -231,13 +231,13 @@ export const UninstallOperatorModal: React.FC = ({ }, [finishVerification, subscriptionName, subscriptionNamespace]); const closeAndRedirect = React.useCallback(() => { - close(); + close?.(); // if url contains subscription name (ex: "codeready-workspaces") or installedCSV version (ex: "crwoperator.v2.9.0") // redirect to ClusterServiceVersion "Installed Operators" list page, // else uninstalled from "Installed Operators" list page, so do not redirect if ( - window.location.pathname.split('/').includes(subscription.metadata.name) || - window.location.pathname.split('/').includes(subscription?.status?.installedCSV) + window.location.pathname.split('/').includes(subscription?.metadata?.name || '') || + window.location.pathname.split('/').includes(subscription?.status?.installedCSV || '') ) { history.push(resourceListPathFromModel(ClusterServiceVersionModel, getActiveNamespace())); } @@ -299,9 +299,9 @@ export const UninstallOperatorModal: React.FC = ({ const name = csv?.spec?.displayName || subscription?.spec?.name; const csvName = csv?.metadata?.name; const namespace = - subscription.metadata.namespace === DEFAULT_GLOBAL_OPERATOR_INSTALLATION_NAMESPACE + subscription?.metadata?.namespace === DEFAULT_GLOBAL_OPERATOR_INSTALLATION_NAMESPACE ? 'all-namespaces' - : subscription.metadata.namespace; + : subscription?.metadata?.namespace || ''; const uninstallMessage = csv?.metadata?.annotations?.[OLMAnnotation.UninstallMessage]; const showOperandsContent = !operandsLoaded || operands.length > 0; @@ -342,8 +342,8 @@ export const UninstallOperatorModal: React.FC = ({ = ({ const operandDeletionAlert = operandDeletionErrors.length ? ( ) : operandDeletionVerificationError ? ( = ({ = ({ operands, loaded, csvName {operands - .sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)) + .sort((a, b) => a.metadata?.name?.localeCompare(b.metadata?.name || '') || 0) .map((operand) => ( - + - + = ({ operands, loaded, csvName {operand.kind} - {operand.metadata.namespace ? ( + {operand.metadata?.namespace ? ( ) : ( @@ -644,10 +644,10 @@ const OperandErrorList: React.FC = ({ operandErrors, csvN
    {_.map(operandErrors, (operandError) => (
  • - {' '} + {' '} {operandError.operand.kind} {' '} {t('olm~Error: {{error}}', { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx index 8ce1ea9e1d6..f21966f68db 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx @@ -77,7 +77,7 @@ export const UpdateStrategyModal: React.FC = ({ errorMessage={errorMessage} inProgress={inProgress} submitText={t('public~Save')} - cancel={cancel} + cancel={cancel as any} /> ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx index 9acaec1077b..d7a357ba98c 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx @@ -117,7 +117,11 @@ const idFromPath = (path) => `DEPRECATED_root_${path.split('.').join('_')}`; * both prefix and suffix are provided, this will return true only if the field has a capability * that matches the concatenation of prefix + suffix. */ -const hasDescriptor = (field: OperandField, prefix: string, suffix: string = null): boolean => { +const hasDescriptor = ( + field: OperandField, + prefix: string, + suffix: string | null = null, +): boolean => { return suffix ? _.includes(field.capabilities, `${prefix}${suffix}`) : _.some(field.capabilities, (capability) => capability.startsWith(prefix)); @@ -137,7 +141,7 @@ const parseGroupDescriptor = ( descriptor.startsWith(SpecCapability.fieldGroup) || descriptor.startsWith(SpecCapability.arrayFieldGroup), ); - const [regexMatch, groupType, groupName] = groupDescriptor.match(GROUP_PATTERN) || []; + const [regexMatch, groupType, groupName] = groupDescriptor?.match(GROUP_PATTERN) || []; return { regexMatch, groupName, groupType }; }; @@ -435,8 +439,8 @@ const specDescriptorToFields = ( displayName, description, capabilities, - type: null, - required: null, + type: null as any, + required: null as any, })), ); } @@ -445,8 +449,8 @@ const specDescriptorToFields = ( path: `spec.${path}`, displayName, description, - type: null, - required: null, + type: null as any, + required: null as any, capabilities, }, ]; @@ -520,16 +524,16 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ // Immutable will not initialize a deep path as a List if it includes an integer, so we need to manually // initialize non-existent array properties to a List instance before updating state at that path. if (regexMatch && index === 0) { - const existing = immutableFormData.getIn([...pathToArray(pathBeforeIndex), 0]); - const item = Immutable.Map(existing || {}).setIn(pathToArray(pathAfterIndex), value); + const existing = immutableFormData.getIn([...pathToArray(pathBeforeIndex || ''), 0]); + const item = Immutable.Map(existing || {}).setIn(pathToArray(pathAfterIndex || ''), value); const list = Immutable.List([item]); - onChange(immutableFormData.setIn(pathToArray(pathBeforeIndex), list).toJS()); + onChange?.(immutableFormData.setIn(pathToArray(pathBeforeIndex || ''), list).toJS()); } - onChange(immutableFormData.setIn(pathToArray(path), value).toJS()); + onChange?.(immutableFormData.setIn(pathToArray(path), value).toJS()); }; - const handleFormDataDelete = (path) => { - onChange(immutableFormData.deleteIn(pathToArray(path)).toJS()); + const handleFormDataDelete = (path: string) => { + onChange?.(immutableFormData.deleteIn(pathToArray(path)).toJS()); }; // Map providedAPI spec descriptors and openAPI spec properties to OperandField[] array @@ -538,7 +542,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ const schemaFields = fieldsForOpenAPI( schema?.properties?.spec as JSONSchema6, providedAPI, - formData, + formData as K8sResourceKind, ); // Get fields from providedAPI that do not exist in the OpenAPI spec. @@ -553,7 +557,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ // Add the field if it doesn't exist return [ ...providedAPIFieldsAccumulator, - ...specDescriptorToFields(specDescriptor, formData), + ...specDescriptorToFields(specDescriptor, formData as K8sResourceKind), ]; }, [], @@ -643,14 +647,17 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ groupName, fieldLists: _.reduce( fieldsInGroup, - (fieldListsAccumulator, field) => { + (fieldListsAccumulator: OperandField[][], field) => { const { index, regexMatch } = parseArrayPath(field.path); if (regexMatch) { - fieldListsAccumulator[index] = [...(fieldListsAccumulator[index] || []), field]; + fieldListsAccumulator[index || 0] = [ + ...(fieldListsAccumulator[index || 0] || []), + field, + ]; } return fieldListsAccumulator; }, - [], + [] as OperandField[][], ), })); }, [arrayFields]); @@ -790,8 +797,10 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ ); } if (capabilities.some((c) => c.startsWith(SpecCapability.k8sResourcePrefix))) { - const groupVersionKind: GroupVersionKind = capabilities - .find((c) => c.startsWith(SpecCapability.k8sResourcePrefix)) + const capability = capabilities.find((c) => c.startsWith(SpecCapability.k8sResourcePrefix)); + if (!capability) return null; + + const groupVersionKind: GroupVersionKind = capability .split(SpecCapability.k8sResourcePrefix)[1] .replace('core~v1~', ''); const k8sModel = modelFor(groupVersionKind); @@ -805,13 +814,13 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ resources={[ { kind: groupVersionKind, - namespace: k8sModel.namespaced ? params?.ns : null, + namespace: k8sModel.namespaced ? params?.ns : undefined, }, ]} desc={displayName} placeholder={t('olm~Select {{item}}', { item: kindForReference(groupVersionKind) })} onChange={(value) => handleFormDataUpdate(path, value)} - selectedKey={currentValue ? `${currentValue}-${k8sModel?.kind}` : null} + selectedKey={currentValue ? `${currentValue}-${k8sModel?.kind}` : undefined} /> ) : null; } @@ -980,7 +989,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ const leftShiftedFields = _.reduce( fieldsToLeftShift, (fieldAccumulator, field) => { - const path = modifyArrayFieldPathIndex(field.path, (index) => index - 1); + const path = modifyArrayFieldPathIndex(field.path, (index) => (index || 1) - 1); return [...fieldAccumulator, { ...field, path }]; }, [], @@ -1010,8 +1019,11 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ const groupDisplayName = _.startCase(groupName); const singularGroupDisplayName = groupDisplayName.replace(/s$/, ''); const id = `root_spec_${groupName}`; - const isExpanded = !_.some(fieldLists, (fieldList) => - _.some(fieldList, (f) => hasDescriptor(f, SpecCapability.advanced) && !f.required), + const isExpanded = !_.some(fieldLists, (fieldList: OperandField[]) => + _.some( + fieldList, + (f) => hasDescriptor(f as OperandField, SpecCapability.advanced) && !f.required, + ), ); return ( @@ -1033,7 +1045,11 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({
)} {_.map(fieldList, (field) => ( - + ))} ))} @@ -1049,7 +1065,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ ); - }); + }).filter(Boolean); const renderFieldGroups = () => _.map(_.sortBy(fieldGroups, 'groupName'), ({ fieldList, groupName }) => { @@ -1064,16 +1080,16 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ return ( {_.map(fieldList, (field) => ( - + ))} ); - }); + }).filter(Boolean); const renderNormalFields = () => _.map(normalFields, (field) => ( - - )); + + )).filter(Boolean); const renderAdvancedFields = () => advancedFields.length > 0 && ( @@ -1083,8 +1099,8 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ textCollapsed={t('olm~Advanced configuration')} > {_.map(advancedFields, (field) => ( - - ))} + + )).filter(Boolean)} ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.spec.tsx index 09a62e88da9..0e42605166a 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.spec.tsx @@ -63,7 +63,10 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2137] CreateOperand', () => it('passes correct YAML to YAML editor', () => { const data = _.cloneDeep(testClusterServiceVersion); const testResourceInstanceYAML = safeDump(testResourceInstance); - data.metadata.annotations = { 'alm-examples': JSON.stringify([testResourceInstance]) }; + data.metadata = { + ...data.metadata, + annotations: { 'alm-examples': JSON.stringify([testResourceInstance]) }, + }; wrapper = wrapper.setProps({ csv: data, loaded: true, loadError: null }); expect(wrapper.find(OperandYAML).props().initialYAML).toEqual(testResourceInstanceYAML); }); @@ -93,7 +96,7 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] CreateOperandForm', ( wrapper = shallow( , @@ -102,7 +105,9 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] CreateOperandForm', ( it('renders form', () => { expect( - referenceForProvidedAPI(testClusterServiceVersion.spec.customresourcedefinitions.owned[0]), + referenceForProvidedAPI( + testClusterServiceVersion.spec?.customresourcedefinitions?.owned?.[0] as any, + ), ).toEqual(k8s.referenceForModel(testModel)); expect(wrapper.find('form').exists()).toBe(true); @@ -110,7 +115,7 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] CreateOperandForm', ( it('renders input component for each field', () => { wrapper.find('.co-dynamic-form__form-group').forEach((formGroup) => { - const descriptor = testClusterServiceVersion.spec.customresourcedefinitions.owned[0].specDescriptors.find( + const descriptor = testClusterServiceVersion.spec?.customresourcedefinitions?.owned?.[0]?.specDescriptors?.find( (d) => d.displayName === formGroup.find('.form-label').text(), ); @@ -131,8 +136,8 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] CreateOperandForm', ( expect(model).toEqual(testModel); expect(obj.apiVersion).toEqual(k8s.apiVersionForModel(testModel)); expect(obj.kind).toEqual(testModel.kind); - expect(obj.metadata.name).toEqual('example'); - expect(obj.metadata.namespace).toEqual('default'); + expect(obj.metadata?.name).toEqual('example'); + expect(obj.metadata?.namespace).toEqual('default'); done(); }) .catch((err) => fail(err)); @@ -157,7 +162,7 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] CreateOperandForm', ( }); }); -describe(OperandYAML.displayName, () => { +describe(OperandYAML.displayName || '', () => { let wrapper: ShallowWrapper; beforeEach(() => { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx index 440872da424..7ea9be06761 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx @@ -59,7 +59,7 @@ export const CreateOperand: React.FC = ({ isList: false, name: nameForModel(model), } - : undefined, + : null, ); const formHelpText = t( @@ -128,7 +128,7 @@ export const CreateOperand: React.FC = ({ ; const CreateOperandPage: React.FC = () => { const { t } = useTranslation(); const params = useParams(); - const createResourceExtension = useCreateResourceExtension(params.plural); + const createResourceExtension = useCreateResourceExtension(params.plural || ''); const { csvName, ns } = useParams(); - const [csv, loaded, loadError] = useClusterServiceVersion(csvName, ns); + const [csv, loaded, loadError] = useClusterServiceVersion(csvName || '', ns || ''); return ( <> - {t('olm~Create {{item}}', { item: kindForReference(params.plural) })} + {t('olm~Create {{item}}', { item: kindForReference(params.plural || '') })} - + {createResourceExtension ? ( ({ const i18nNS = 'public'; -describe(OperandTableRow.displayName, () => { +describe(OperandTableRow.displayName || '', () => { let wrapper: ReactWrapper; beforeEach(() => { @@ -152,7 +152,7 @@ describe(OperandTableRow.displayName, () => { const col = wrapper.childAt(2); const link = col.find(ResourceLink); - expect(link.props().name).toEqual(testResourceInstance.metadata.namespace); + expect(link.props().name).toEqual(testResourceInstance.metadata?.namespace); }); it('renders column for resource status', () => { const col = wrapper.childAt(3); @@ -165,14 +165,14 @@ describe(OperandTableRow.displayName, () => { const labelList = col.find(LabelList); expect(labelList.props().kind).toEqual(testResourceInstance.kind); - expect(labelList.props().labels).toEqual(testResourceInstance.metadata.labels); + expect(labelList.props().labels).toEqual(testResourceInstance.metadata?.labels); }); it('renders column for last updated timestamp', () => { const col = wrapper.childAt(5); const timestamp = col.find(Timestamp); - expect(timestamp.props().timestamp).toEqual(testResourceInstance.metadata.creationTimestamp); + expect(timestamp.props().timestamp).toEqual(testResourceInstance.metadata?.creationTimestamp); }); it('renders a `LazyActionsMenu` for resource actions', () => { @@ -181,7 +181,7 @@ describe(OperandTableRow.displayName, () => { }); }); -describe(OperandList.displayName, () => { +describe(OperandList.displayName || '', () => { let wrapper: ReactWrapper; let resources: K8sResourceKind[]; @@ -217,14 +217,14 @@ describe(OperandList.displayName, () => { }); }); -describe(OperandDetails.displayName, () => { +describe(OperandDetails.displayName || '', () => { let wrapper: ShallowWrapper; let resourceDefinition: any; beforeEach(() => { resourceDefinition = { plural: testModel.plural, - annotations: testCRD.metadata.annotations, + annotations: testCRD.metadata?.annotations, apiVersion: testModel.apiVersion, }; wrapper = shallow( @@ -233,7 +233,7 @@ describe(OperandDetails.displayName, () => { crd={testCRD} obj={testResourceInstance} kindObj={resourceDefinition} - appName={testClusterServiceVersion.metadata.name} + appName={testClusterServiceVersion.metadata?.name || ''} />, ); }); @@ -250,10 +250,10 @@ describe(OperandDetails.displayName, () => { }); it('does not render filtered status fields', () => { - const crd = testClusterServiceVersion.spec.customresourcedefinitions.owned.find( + const crd = testClusterServiceVersion.spec?.customresourcedefinitions?.owned?.find( (c) => c.name === 'testresources.testapp.coreos.com', ); - const filteredDescriptor = crd.statusDescriptors.find((sd) => sd.path === 'importantMetrics'); + const filteredDescriptor = crd?.statusDescriptors?.find((sd) => sd.path === 'importantMetrics'); const statusView = wrapper .find(DescriptorDetailsItem) .filterWhere((node) => node.props().descriptor === filteredDescriptor); @@ -263,7 +263,9 @@ describe(OperandDetails.displayName, () => { it('does not render any spec descriptor fields if there are none defined on the `ClusterServiceVersion`', () => { const csv = _.cloneDeep(testClusterServiceVersion); - csv.spec.customresourcedefinitions.owned = []; + if (csv.spec?.customresourcedefinitions?.owned) { + csv.spec.customresourcedefinitions.owned = []; + } wrapper = wrapper.setProps({ csv }); expect(wrapper.find(DescriptorDetailsItem).length).toEqual(0); @@ -273,28 +275,31 @@ describe(OperandDetails.displayName, () => { expect( wrapper.find(DescriptorDetailsItemList).last().shallow().find(DescriptorDetailsItem).length, ).toEqual( - testClusterServiceVersion.spec.customresourcedefinitions.owned[0].specDescriptors.length, + testClusterServiceVersion.spec?.customresourcedefinitions?.owned?.[0]?.specDescriptors + ?.length, ); }); xit('[CONSOLE-2336] renders spec descriptor fields if the custom resource is `required`', () => { const csv = _.cloneDeep(testClusterServiceVersion); - csv.spec.customresourcedefinitions.required = _.cloneDeep( - csv.spec.customresourcedefinitions.owned, - ); - csv.spec.customresourcedefinitions.owned = []; + if (csv.spec?.customresourcedefinitions?.owned) { + csv.spec.customresourcedefinitions.required = _.cloneDeep( + csv.spec.customresourcedefinitions.owned, + ); + csv.spec.customresourcedefinitions.owned = []; + } wrapper = wrapper.setProps({ csv }); expect( wrapper.find(DescriptorDetailsItemList).last().shallow().find(DescriptorDetailsItem).length, - ).toEqual(csv.spec.customresourcedefinitions.required[0].specDescriptors.length); + ).toEqual(csv.spec?.customresourcedefinitions?.required?.[0]?.specDescriptors?.length); }); it('renders a Condtions table', () => { expect(wrapper.find('SectionHeading').at(1).prop('text')).toEqual('Conditions'); expect(wrapper.find('Conditions').prop('conditions')).toEqual( - testResourceInstance.status.conditions, + testResourceInstance.status?.conditions, ); }); @@ -332,7 +337,7 @@ describe('ResourcesList', () => { ); const multiListPage = wrapper.find(MultiListPage); expect(multiListPage.props().resources).toEqual( - testClusterServiceVersion.spec.customresourcedefinitions.owned[0].resources.map( + testClusterServiceVersion.spec?.customresourcedefinitions?.owned?.[0]?.resources?.map( (resource) => ({ kind: resource.kind, namespaced: true, prop: 'Pod' }), ), ); @@ -377,12 +382,12 @@ describe(OperandDetailsPage.displayName, () => { ); const detailsPage = wrapper.find(DetailsPage); - expect(detailsPage.props().pages[0].nameKey).toEqual(`${i18nNS}~Details`); - expect(detailsPage.props().pages[0].href).toEqual(''); - expect(detailsPage.props().pages[1].nameKey).toEqual(`${i18nNS}~YAML`); - expect(detailsPage.props().pages[1].href).toEqual('yaml'); - expect(detailsPage.props().pages[2].nameKey).toEqual('olm~Resources'); - expect(detailsPage.props().pages[2].href).toEqual('resources'); + expect(detailsPage.props().pages?.[0]?.nameKey).toEqual(`${i18nNS}~Details`); + expect(detailsPage.props().pages?.[0]?.href).toEqual(''); + expect(detailsPage.props().pages?.[1]?.nameKey).toEqual(`${i18nNS}~YAML`); + expect(detailsPage.props().pages?.[1]?.href).toEqual('yaml'); + expect(detailsPage.props().pages?.[2]?.nameKey).toEqual('olm~Resources'); + expect(detailsPage.props().pages?.[2]?.href).toEqual('resources'); }); it('renders a `DetailsPage` which also watches the parent CSV', () => { @@ -393,7 +398,7 @@ describe(OperandDetailsPage.displayName, () => { , ); - expect(wrapper.find(DetailsPage).prop('resources')[0]).toEqual({ + expect(wrapper.find(DetailsPage).prop('resources')?.[0]).toEqual({ kind: 'CustomResourceDefinition', name: 'testresources.testapp.coreos.com', isList: false, @@ -420,7 +425,12 @@ describe(OperandDetailsPage.displayName, () => { , ); - expect(wrapper.find(DetailsPage).props().breadcrumbsFor(null)).toEqual([ + expect( + wrapper + .find(DetailsPage) + .props() + .breadcrumbsFor?.(null as any), + ).toEqual([ { name: 'Installed Operators', path: `/k8s/ns/default/${ClusterServiceVersionModel.plural}`, @@ -456,7 +466,12 @@ describe(OperandDetailsPage.displayName, () => { , ); - expect(wrapper.find(DetailsPage).props().breadcrumbsFor(null)).toEqual([ + expect( + wrapper + .find(DetailsPage) + .props() + .breadcrumbsFor?.(null as any), + ).toEqual([ { name: 'Installed Operators', path: `/k8s/ns/example/${ClusterServiceVersionModel.plural}`, @@ -501,7 +516,7 @@ describe(OperandDetailsPage.displayName, () => { uid: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', ownerReferences: [ { - uid: testResourceInstance.metadata.uid, + uid: testResourceInstance.metadata?.uid || '', name: 'foo', kind: 'fooKind', apiVersion: 'fooVersion', @@ -519,20 +534,20 @@ describe(OperandDetailsPage.displayName, () => { Deployment: { data: [deployment], loaded: true, - loadError: undefined, + loadError: undefined as any, }, Secret: { data: [secret], loaded: true, - loadError: undefined, + loadError: undefined as any, }, Pod: { data: [pod], loaded: true, - loadError: undefined, + loadError: undefined as any, }, }; - const data = flatten(resources); + const data = flatten?.(resources); expect(data.map((obj) => obj.metadata.uid)).not.toContain(secret.metadata.uid); expect(data.map((obj) => obj.metadata.uid)).toContain(pod.metadata.uid); @@ -580,7 +595,7 @@ describe(ProvidedAPIsPage.displayName, () => { it('passes `items` props and render ListPageCreateDropdown create button if app has multiple owned CRDs', () => { const obj = _.cloneDeep(testClusterServiceVersion); - obj.spec.customresourcedefinitions.owned.push({ + obj.spec?.customresourcedefinitions?.owned?.push({ name: 'foobars.testapp.coreos.com', displayName: 'Foo Bars', version: 'v1', diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx index d59e94c9534..10a81c76fb0 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx @@ -57,6 +57,7 @@ import { CustomResourceDefinitionKind, definitionFor, K8sResourceCommon, + K8sResourceKindReference, } from '@console/internal/module/k8s'; import { Status, @@ -97,7 +98,7 @@ const tableColumnClasses = [ Kebab.columnClass, ]; -const getOperandStatus = (obj: K8sResourceKind): OperandStatusType => { +const getOperandStatus = (obj: K8sResourceKind): OperandStatusType | null => { const { phase, status, state, conditions } = obj?.status || {}; if (phase && _.isString(phase)) { @@ -146,7 +147,7 @@ const hasAllNamespaces = (csv: ClusterServiceVersionKind) => { }; export const OperandStatus: React.FC = ({ operand }) => { - const status: OperandStatusType = getOperandStatus(operand); + const status: OperandStatusType | null = getOperandStatus(operand); if (!status) { return <>-; } @@ -182,11 +183,11 @@ export const OperandTableRow: React.FC = ({ obj, showNames
{showNamespace && ( - {obj.metadata.namespace ? ( + {obj.metadata?.namespace ? ( ) : ( '-' @@ -197,10 +198,13 @@ export const OperandTableRow: React.FC = ({ obj, showNames - + - + @@ -280,10 +284,10 @@ export const OperandList: React.FC = (props) => { if (obj.apiVersion && obj.kind) { return obj; } - const reference = props.kinds[0]; + const reference = props.kinds?.[0]; return { - apiVersion: apiVersionForReference(reference), - kind: kindForReference(reference), + apiVersion: apiVersionForReference(reference as any), + kind: kindForReference(reference as any), ...obj, }; }) ?? [], @@ -295,7 +299,7 @@ export const OperandList: React.FC = (props) => { {...props} customSorts={{ operandStatus: getOperandStatusText, - getOperandNamespace, + getOperandNamespace: getOperandNamespace as (obj: any) => string, }} data={data} EmptyMsg={() => @@ -366,7 +370,7 @@ export const ProvidedAPIsPage = (props: ProvidedAPIsPageProps) => { const providedAPIs = providedAPIsForCSV(obj); const owners = (ownerRefs: OwnerReference[], items: K8sResourceKind[]) => - ownerRefs.filter(({ uid }) => items.filter(({ metadata }) => metadata.uid === uid).length > 0); + ownerRefs.filter(({ uid }) => items.filter(({ metadata }) => metadata?.uid === uid).length > 0); const flatten: Flatten<{ [key: string]: K8sResourceCommon[]; }> = React.useCallback( @@ -374,7 +378,7 @@ export const ProvidedAPIsPage = (props: ProvidedAPIsPageProps) => { _.flatMap(resources, (resource) => _.map(resource.data, (item) => item)).filter( ({ kind, metadata }, i, allResources) => providedAPIs.filter((item) => item.kind === kind).length > 0 || - owners(metadata.ownerReferences || [], allResources).length > 0, + owners(metadata?.ownerReferences || [], allResources).length > 0, ), [providedAPIs], ); @@ -391,7 +395,7 @@ export const ProvidedAPIsPage = (props: ProvidedAPIsPageProps) => { const watchedResources = getK8sWatchResources( models, providedAPIs, - listAllNamespaces ? null : namespace, + listAllNamespaces ? undefined : namespace, ); const resources = useK8sWatchResources<{ [key: string]: K8sResourceKind[] }>(watchedResources); @@ -448,7 +452,7 @@ export const ProvidedAPIsPage = (props: ProvidedAPIsPageProps) => { return inFlight ? null : ( <> } > @@ -467,7 +471,7 @@ export const ProvidedAPIsPage = (props: ProvidedAPIsPageProps) => { hideColumnManagement={hideColumnManagement} /> = (props) => return ( <> } > @@ -594,7 +598,7 @@ const PodStatuses: React.FC = ({ kindObj, obj, podStatusDescri descriptor={statusDescriptor} model={kindObj} obj={obj} - schema={schema} + schema={schema as JSONSchema7} /> ); @@ -605,7 +609,7 @@ const PodStatuses: React.FC = ({ kindObj, obj, podStatusDescri export const OperandDetails = connectToModel(({ crd, csv, kindObj, obj }: OperandDetailsProps) => { const { t } = useTranslation(); const { kind, status } = obj; - const [errorMessage, setErrorMessage] = React.useState(null); + const [errorMessage, setErrorMessage] = React.useState(null); const handleError = (err: Error) => setErrorMessage(err.message); // Find the matching CRD spec for the kind of this resource in the CSV. @@ -705,7 +709,7 @@ export const OperandDetails = connectToModel(({ crd, csv, kindObj, obj }: Operan { const context = { [referenceForModel(resourceModel)]: resource, @@ -755,8 +759,8 @@ const DefaultOperandDetailsPage = ({ k8sModel }: DefaultOperandDetailsPageProps) return ( { const { plural, ns, name } = useParams(); - const resourceDetailsPage = useResourceDetailsPage(plural); + const resourceDetailsPage = useResourceDetailsPage(plural as any); const [k8sModel, inFlight] = useK8sModel(plural); if (inFlight && !k8sModel) { return null; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx index 53b40d268be..c29ada4bbf5 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx @@ -54,8 +54,8 @@ export const OperandForm: React.FC = ({ history.replace( resourcePathFromModel( ClusterServiceVersionModel, - csv.metadata.name, - csv.metadata.namespace, + csv.metadata?.name || '', + csv.metadata?.namespace || '', ), ); } else { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx index 1e1426549e8..83477bbf415 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx @@ -15,7 +15,7 @@ export const csvNameFromWindow = () => ); export const OperandLink: React.FC = (props) => { - const { namespace, name } = props.obj.metadata; + const { namespace, name } = props.obj.metadata || { namespace: '', name: '' }; const csvName = props.csvName || csvNameFromWindow(); const reference = referenceFor(props.obj); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/utils.spec.ts b/frontend/packages/operator-lifecycle-manager/src/components/operand/utils.spec.ts index 1b34e5d1bff..b6a084bc789 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/utils.spec.ts +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/utils.spec.ts @@ -96,26 +96,26 @@ describe('capabilitiesToUISchema', () => { `${SpecCapability.k8sResourcePrefix}ServiceAccount` as SpecCapability, ]); expect(uiSchema['ui:widget']).toEqual('K8sResourceWidget'); - expect(uiSchema['ui:options'].model).toEqual(ServiceAccountModel); - expect(uiSchema['ui:options'].groupVersionKind).toEqual('ServiceAccount'); + expect(uiSchema['ui:options']?.model).toEqual(ServiceAccountModel); + expect(uiSchema['ui:options']?.groupVersionKind).toEqual('ServiceAccount'); }); it('Handles SpecCapability.k8sResourcePrefix with equality-based label queries', () => { const uiSchema = capabilitiesToUISchema([ `${SpecCapability.k8sResourcePrefix}ServiceAccount?label!=test,level=production` as SpecCapability, ]); expect(uiSchema['ui:widget']).toEqual('K8sResourceWidget'); - expect(uiSchema['ui:options'].model).toEqual(ServiceAccountModel); - expect(uiSchema['ui:options'].groupVersionKind).toEqual('ServiceAccount'); - expect(uiSchema['ui:options'].selector).toEqual('label!=test,level=production'); + expect(uiSchema['ui:options']?.model).toEqual(ServiceAccountModel); + expect(uiSchema['ui:options']?.groupVersionKind).toEqual('ServiceAccount'); + expect(uiSchema['ui:options']?.selector).toEqual('label!=test,level=production'); }); it('Handles SpecCapability.k8sResourcePrefix with set-based label queries', () => { const uiSchema = capabilitiesToUISchema([ `${SpecCapability.k8sResourcePrefix}ServiceAccount?level in (production,qa)` as SpecCapability, ]); expect(uiSchema['ui:widget']).toEqual('K8sResourceWidget'); - expect(uiSchema['ui:options'].model).toEqual(ServiceAccountModel); - expect(uiSchema['ui:options'].groupVersionKind).toEqual('ServiceAccount'); - expect(uiSchema['ui:options'].selector).toEqual('level in (production,qa)'); + expect(uiSchema['ui:options']?.model).toEqual(ServiceAccountModel); + expect(uiSchema['ui:options']?.groupVersionKind).toEqual('ServiceAccount'); + expect(uiSchema['ui:options']?.selector).toEqual('level in (production,qa)'); }); it('Handles SpecCapablitiy.select', () => { const uiSchema = capabilitiesToUISchema([ diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx index 9cf54715495..eac9cf7cea2 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx @@ -245,7 +245,7 @@ const determineAvailableFilters = ( const filters = _.cloneDeep(initialFilters); _.each(filterGroups, (field) => { - const values = []; + const values: { label: any; synonyms?: any; value: any; active: boolean }[] = []; _.each(items, (item) => { let value = item[field]; let synonyms; @@ -571,14 +571,14 @@ const OperatorHubTile: React.FC = ({ item, onClick }) => { export const OperatorHubTileView: React.FC = (props) => { const { t } = useTranslation(); - const [detailsItem, setDetailsItem] = React.useState(null); + const [detailsItem, setDetailsItem] = React.useState(null); const [showDetails, setShowDetails] = React.useState(false); const [ignoreOperatorWarning, setIgnoreOperatorWarning, loaded] = useUserSettingsCompatibility< boolean >(userSettingsKey, storeKey, false); const [updateChannel, setUpdateChannel] = React.useState(''); const [updateVersion, setUpdateVersion] = React.useState(''); - const [tokenizedAuth, setTokenizedAuth] = React.useState(null); + const [tokenizedAuth, setTokenizedAuth] = React.useState(null); const installVersion = getQueryArgument('version'); const filteredItems = filterByArchAndOS(props.items); @@ -709,7 +709,7 @@ export const OperatorHubTileView: React.FC = (props) = const currentItem = _.find(filteredItems, { uid: detailsItemID, }); - setDetailsItem(currentItem); + setDetailsItem(currentItem as OperatorHubItem); setShowDetails(!_.isNil(currentItem)); if ( currentItem && @@ -793,23 +793,30 @@ export const OperatorHubTileView: React.FC = (props) = const installParamsURL = detailsItem && detailsItem.obj && - new URLSearchParams({ - pkg: detailsItem.obj.metadata.name, - catalog: detailsItem.catalogSource, - catalogNamespace: detailsItem.catalogSourceNamespace, - targetNamespace: props.namespace, - channel: updateChannel, - version: updateVersion, - tokenizedAuth, - }).toString(); + new URLSearchParams( + Object.entries({ + pkg: detailsItem.obj.metadata?.name, + catalog: detailsItem.catalogSource, + catalogNamespace: detailsItem.catalogSourceNamespace, + targetNamespace: props.namespace, + channel: updateChannel, + version: updateVersion, + tokenizedAuth, + }).reduce((acc, [key, value]) => { + if (value !== undefined && value !== '') { + acc[key] = value as string; + } + return acc; + }, {} as Record), + ).toString(); const installLink = - detailsItem && detailsItem.obj && `/operatorhub/subscribe?${installParamsURL.toString()}`; + detailsItem && detailsItem.obj && `/operatorhub/subscribe?${installParamsURL}`; const uninstallLink = () => detailsItem && detailsItem.subscription && - `/k8s/ns/${detailsItem.subscription.metadata.namespace}/${SubscriptionModel.plural}/${detailsItem.subscription.metadata.name}?showDelete=true`; + `/k8s/ns/${detailsItem.subscription.metadata?.namespace}/${SubscriptionModel.plural}/${detailsItem.subscription.metadata?.name}?showDelete=true`; if (_.isEmpty(filteredItems)) { return ( @@ -849,7 +856,7 @@ export const OperatorHubTileView: React.FC = (props) = const titleAndDeprecatedPackage = () => ( <> - {detailsItem.name} + {detailsItem?.name} {detailsItem?.obj?.status?.deprecation && ( = (props) = /* eslint-disable */ // CONSOLE TABLE FOR TESTING - Using memoized sorted data for performance // Displays search results with 'Search Relevance Score' and 'Is Red Hat' provider priority values, used in determining display order of operators - + const getActiveFiltersDescription = () => { const activeFilters = []; if (selectedCategory !== 'all') activeFilters.push(`Category: ${selectedCategory}`); @@ -871,10 +878,10 @@ export const OperatorHubTileView: React.FC = (props) = if (selectedCapabilityLevel !== 'all') activeFilters.push(`Capability Level: ${selectedCapabilityLevel}`); if (selectedInfraFeatures !== 'all') activeFilters.push(`Infrastructure Features: ${selectedInfraFeatures}`); if (selectedValidSubscriptionFilters !== 'all') activeFilters.push(`Valid Subscription: ${selectedValidSubscriptionFilters}`); - + return activeFilters.length > 0 ? activeFilters.join(', ') : 'No filters applied'; }; - + // Debug logging for non-production environments if (process.env.NODE_ENV !== 'production') { if (searchTerm) { @@ -890,7 +897,7 @@ export const OperatorHubTileView: React.FC = (props) = } else { console.log('šŸ“‹ OperatorHub Items Array:', sortedItems.map(item => item.name || 'N/A')); } - + // Debug: Log component state to identify race conditions console.log('šŸ› Debug Info:', { searchTerm, @@ -899,7 +906,7 @@ export const OperatorHubTileView: React.FC = (props) = hasSearchTerm: !!searchTerm, isInitialLoad: (!filteredItems || filteredItems.length === 0) }); - + console.log(`šŸ“Œ Current Active Filters: ${getActiveFiltersDescription()}`); console.log(`šŸ” Search Term: "${searchTerm || 'none'}"`); console.log(`šŸ“Š Sorted Items Count: ${sortedItems.length}`); @@ -913,14 +920,14 @@ export const OperatorHubTileView: React.FC = (props) = relevanceScore: calculateRelevanceScore(searchTerm, item), })) .filter((item) => item.relevanceScore > 0); - + // Sort the filtered items the same way TileViewPage will sort them const searchSortedItems = orderAndSortByRelevance(searchFilteredItems, searchTerm); - + const tableData = searchSortedItems.map((item, index) => ({ Title: item.name || 'N/A', 'Search Relevance Score': item.relevanceScore || 0, - 'Is Red Hat Provider (Priority)': getRedHatPriority(item) === REDHAT_PRIORITY.EXACT_MATCH ? `Exact Match (${REDHAT_PRIORITY.EXACT_MATCH})` : + 'Is Red Hat Provider (Priority)': getRedHatPriority(item) === REDHAT_PRIORITY.EXACT_MATCH ? `Exact Match (${REDHAT_PRIORITY.EXACT_MATCH})` : getRedHatPriority(item) === REDHAT_PRIORITY.CONTAINS_REDHAT ? `Contains Red Hat (${REDHAT_PRIORITY.CONTAINS_REDHAT})` : `Non-Red Hat (${REDHAT_PRIORITY.NON_REDHAT})`, // Source: item.source || 'N/A', 'Metadata Provider': _.get(item, 'obj.metadata.labels.provider', 'N/A'), @@ -933,18 +940,18 @@ export const OperatorHubTileView: React.FC = (props) = return itemCapability; } } - + const specCapability = _.get(item, 'obj.spec.capabilityLevel', ''); if (specCapability) return specCapability; - + const metadataCapability = _.get(item, 'obj.metadata.annotations["operators.operatorframework.io/capability-level"]', ''); if (metadataCapability) return metadataCapability; - + return 'N/A'; })(), 'Infrastructure Features': Array.isArray(item.infraFeatures) ? item.infraFeatures.join(', ') : 'N/A', })); - + console.log(`\nšŸ” OperatorHub Search Results for "${searchTerm}" (${searchSortedItems.length} matches)`); console.log(`šŸ“Œ Active Filters: ${getActiveFiltersDescription()}`); console.table(tableData); @@ -952,7 +959,7 @@ export const OperatorHubTileView: React.FC = (props) = // Console table for filtered results without search term (category/filter-based) - using memoized data const tableData = sortedItems.map((item, index) => ({ Title: item.name || 'N/A', - 'Is Red Hat Provider (Priority)': getRedHatPriority(item) === REDHAT_PRIORITY.EXACT_MATCH ? `Exact Match (${REDHAT_PRIORITY.EXACT_MATCH})` : + 'Is Red Hat Provider (Priority)': getRedHatPriority(item) === REDHAT_PRIORITY.EXACT_MATCH ? `Exact Match (${REDHAT_PRIORITY.EXACT_MATCH})` : getRedHatPriority(item) === REDHAT_PRIORITY.CONTAINS_REDHAT ? `Contains Red Hat (${REDHAT_PRIORITY.CONTAINS_REDHAT})` : `Non-Red Hat (${REDHAT_PRIORITY.NON_REDHAT})`, // Source: item.source || 'N/A', 'Metadata Provider': _.get(item, 'obj.metadata.labels.provider', 'N/A'), @@ -965,18 +972,18 @@ export const OperatorHubTileView: React.FC = (props) = return itemCapability; } } - + const specCapability = _.get(item, 'obj.spec.capabilityLevel', ''); if (specCapability) return specCapability; - + const metadataCapability = _.get(item, 'obj.metadata.annotations["operators.operatorframework.io/capability-level"]', ''); if (metadataCapability) return metadataCapability; - + return 'N/A'; })(), 'Infrastructure Features': Array.isArray(item.infraFeatures) ? item.infraFeatures.join(', ') : 'N/A', })); - + console.log(`\nšŸ“‚ OperatorHub Filtered Results (${tableData.length} items)`); console.log(`šŸ“Œ Active Filters: ${getActiveFiltersDescription()}`); console.table(tableData); @@ -1035,7 +1042,7 @@ export const OperatorHubTileView: React.FC = (props) = 'co-catalog-page__overlay-action', )} data-test-id="operator-install-btn" - to={installLink} + to={installLink as any} > {t('olm~Install')} @@ -1044,7 +1051,7 @@ export const OperatorHubTileView: React.FC = (props) = className="co-catalog-page__overlay-action" data-test-id="operator-uninstall-btn" isDisabled={!detailsItem.installed} - onClick={() => history.push(uninstallLink())} + onClick={() => history.push(uninstallLink() as any)} variant="secondary" > {t('olm~Uninstall')} diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-page.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-page.tsx index b7b413d48a3..e4462edb5fc 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-page.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-page.tsx @@ -53,19 +53,21 @@ const clusterServiceVersionFor = ( clusterServiceVersions: ClusterServiceVersionKind[], csvName: string, ): ClusterServiceVersionKind => - clusterServiceVersions?.find((csv) => csv.metadata.name === csvName); + clusterServiceVersions?.find( + (csv) => csv.metadata?.name === csvName, + ) as ClusterServiceVersionKind; const onInfrastructureFeaturesAnnotationError = (error: Error, pkg: PackageManifestKind) => // eslint-disable-next-line no-console console.warn( - `Error parsing infrastructure features from PackageManifest "${pkg.metadata.name}":`, + `Error parsing infrastructure features from PackageManifest "${pkg.metadata?.name}":`, error, ); const onValidSubscriptionAnnotationError = (error: Error, pkg: PackageManifestKind) => // eslint-disable-next-line no-console console.warn( - `Error parsing valid subscription from PackageManifest "${pkg.metadata.name}":`, + `Error parsing valid subscription from PackageManifest "${pkg.metadata?.name}":`, error, ); @@ -105,9 +107,21 @@ export const OperatorHubList: React.FC = ({ const packageManifests = props.packageManifests?.data ?? []; const marketplacePackageManifests = props.marketplacePackageManifests?.data ?? []; const allPackageManifests = [...marketplacePackageManifests, ...packageManifests]; - const clusterIsAWSSTS = isAWSSTSCluster(cloudCredentials, infrastructure, authentication); - const clusterIsAzureWIF = isAzureWIFCluster(cloudCredentials, infrastructure, authentication); - const clusterIsGCPWIF = isGCPWIFCluster(cloudCredentials, infrastructure, authentication); + const clusterIsAWSSTS = isAWSSTSCluster( + cloudCredentials as CloudCredentialKind, + infrastructure as InfrastructureKind, + authentication as AuthenticationKind, + ); + const clusterIsAzureWIF = isAzureWIFCluster( + cloudCredentials as CloudCredentialKind, + infrastructure as InfrastructureKind, + authentication as AuthenticationKind, + ); + const clusterIsGCPWIF = isGCPWIFCluster( + cloudCredentials as CloudCredentialKind, + infrastructure as InfrastructureKind, + authentication as AuthenticationKind, + ); return allPackageManifests .filter((pkg) => { const { channels, defaultChannel } = pkg.status ?? {}; @@ -115,12 +129,12 @@ export const OperatorHubList: React.FC = ({ if (!defaultChannel) { // eslint-disable-next-line no-console console.warn( - `PackageManifest ${pkg.metadata.name} has no status.defaultChannel and has been excluded`, + `PackageManifest ${pkg.metadata?.name} has no status.defaultChannel and has been excluded`, ); return false; } - const { currentCSVDesc } = channels.find((ch) => ch.name === defaultChannel); + const { currentCSVDesc } = channels.find((ch) => ch.name === defaultChannel) as any; // if CSV contains annotation for a non-standalone operator, filter it out return !( currentCSVDesc.annotations?.[OLMAnnotation.OperatorType] === @@ -133,9 +147,14 @@ export const OperatorHubList: React.FC = ({ loaded && subscriptionFor(subscriptions)(operatorGroups)(pkg)(namespace); const clusterServiceVersion = loaded && - clusterServiceVersionFor(clusterServiceVersions, subscription?.status?.installedCSV); + clusterServiceVersionFor( + clusterServiceVersions, + subscription?.status?.installedCSV || '', + ); const { channels, defaultChannel } = pkg.status ?? {}; - const { currentCSVDesc } = (channels || []).find(({ name }) => name === defaultChannel); + const { currentCSVDesc } = (channels || []).find( + ({ name }) => name === defaultChannel, + ) as any; const currentCSVAnnotations: CSVAnnotations = currentCSVDesc?.annotations ?? {}; const infraFeatures = getInfrastructureFeatures(currentCSVAnnotations, { clusterIsAWSSTS, @@ -165,7 +184,7 @@ export const OperatorHubList: React.FC = ({ const installed = loaded && clusterServiceVersion?.status?.phase === 'Succeeded'; return { - authentication, + authentication: authentication as AuthenticationKind, capabilityLevel, catalogSource: pkg.status.catalogSource, catalogSourceNamespace: pkg.status.catalogSourceNamespace, @@ -173,14 +192,14 @@ export const OperatorHubList: React.FC = ({ .split(',') .map((category) => category.trim()), certifiedLevel, - cloudCredentials, + cloudCredentials: cloudCredentials as CloudCredentialKind, containerImage, createdAt, description: currentCSVAnnotations.description || currentCSVDesc.description, healthIndex, imgUrl: iconFor(pkg), infraFeatures, - infrastructure, + infrastructure: infrastructure as InfrastructureKind, installed, installState: installed ? InstalledState.Installed : InstalledState.NotInstalled, isInstalling: @@ -194,15 +213,15 @@ export const OperatorHubList: React.FC = ({ marketplaceActionText, marketplaceRemoteWorkflow, marketplaceSupportWorkflow, - name: currentCSVDesc?.displayName ?? pkg.metadata.name, + name: currentCSVDesc?.displayName ?? pkg.metadata?.name, obj: pkg, - provider: pkg.status.provider?.name ?? pkg.metadata.labels?.provider, + provider: pkg.status.provider?.name ?? pkg.metadata?.labels?.provider, repository, source: getPackageSource(pkg), subscription, support, tags: [], - uid: `${pkg.metadata.name}-${pkg.status.catalogSource}-${pkg.status.catalogSourceNamespace}`, + uid: `${pkg.metadata?.name}-${pkg.status.catalogSource}-${pkg.status.catalogSourceNamespace}`, validSubscription, validSubscriptionFilters, version: currentCSVDesc?.version, diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts index cae3a5a0fde..ab280ca2e68 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts @@ -23,7 +23,7 @@ import { InfrastructureFeature, OLMAnnotation, ValidSubscriptionValue } from '.' describe('getPackageSource', () => { it('should handle undefined argument', () => { - const source = getPackageSource(undefined); + const source = getPackageSource(undefined as any); expect(source).toBeUndefined(); }); it('should return correct default Red Hat operator sources', () => { @@ -53,7 +53,7 @@ describe('getPackageSource', () => { describe('isAWSSTSCluster', () => { it('should handle undefined arguments', () => { - const result = isAWSSTSCluster(undefined, undefined, undefined); + const result = isAWSSTSCluster(undefined as any, undefined as any, undefined as any); expect(result).toEqual(false); }); it('should return true', () => { @@ -88,7 +88,7 @@ describe('isAWSSTSCluster', () => { describe('isAzureWIFCluster', () => { it('should handle undefined arguments', () => { - const result = isAzureWIFCluster(undefined, undefined, undefined); + const result = isAzureWIFCluster(undefined as any, undefined as any, undefined as any); expect(result).toEqual(false); }); it('should return true', () => { @@ -123,7 +123,7 @@ describe('isAzureWIFCluster', () => { describe('isGCPWIFCluster', () => { it('should handle undefined arguments', () => { - const result = isGCPWIFCluster(undefined, undefined, undefined); + const result = isGCPWIFCluster(undefined as any, undefined as any, undefined as any); expect(result).toEqual(false); }); it('should return true', () => { @@ -232,7 +232,7 @@ describe('getClusterServiceVersionPlugins', () => { expect(result).toEqual([]); }); it('returns an empty array when annotations are null', () => { - const result = getClusterServiceVersionPlugins(null); + const result = getClusterServiceVersionPlugins(null as any); expect(result).toEqual([]); }); it('returns an empty array when annotations are undefined', () => { @@ -271,7 +271,7 @@ describe('getInternalObjects', () => { expect(result).toEqual([]); }); it('returns an empty array when annotations are null', () => { - const result = getInternalObjects(null); + const result = getInternalObjects(null as any); expect(result).toEqual([]); }); it('returns an empty array when annotations are undefined', () => { @@ -310,7 +310,7 @@ describe('getSuggestedNamespaceTemplate', () => { expect(result).toBeNull(); }); it('returns null when annotations are null', () => { - const result = getSuggestedNamespaceTemplate(null); + const result = getSuggestedNamespaceTemplate(null as any); expect(result).toBeNull(); }); it('returns null when annotations are undefined', () => { @@ -349,7 +349,7 @@ describe('getInitializationResource', () => { expect(result).toBeNull(); }); it('returns null when annotations are null', () => { - const result = getInitializationResource(null); + const result = getInitializationResource(null as any); expect(result).toBeNull(); }); it('returns null when annotations are undefined', () => { @@ -440,7 +440,7 @@ describe('getValidSubscription', () => { expect(filters).toEqual([]); }); it('returns an empty when annotations are null', () => { - const [subscriptions, filters] = getValidSubscription(null); + const [subscriptions, filters] = getValidSubscription(null as any); expect(subscriptions).toEqual([]); expect(filters).toEqual([]); }); @@ -638,7 +638,7 @@ describe('getInfrastructureFeatures', () => { expect(result).toEqual([]); }); it('returns an empty array when annotations are null', () => { - const result = getInfrastructureFeatures(null); + const result = getInfrastructureFeatures(null as any); expect(result).toEqual([]); }); it('returns an empty array when annotations are undefined', () => { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx index d830d33373d..6c20af3b6c3 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx @@ -28,6 +28,7 @@ import { OperatorHubTileViewProps, } from './operator-hub-items'; import { OperatorHubList, OperatorHubListProps } from './operator-hub-page'; +import { OperatorHubItem } from '.'; xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] OperatorHubList', () => { let wrapper: ReactWrapper; @@ -37,8 +38,10 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] OperatorHubList', () , ); @@ -63,7 +66,7 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] OperatorHubList', () ); expect(amqTileProps.iconClass).toBe(null); expect(amqTileProps.vendor).toEqual( - `provided by ${amqPackageManifest.metadata.labels.provider}`, + `provided by ${amqPackageManifest.metadata?.labels?.provider}`, ); expect( amqTileProps.description.startsWith( @@ -85,7 +88,7 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] OperatorHubList', () ); expect(prometheusTileProps.iconClass).toBe(null); expect(prometheusTileProps.vendor).toEqual( - `provided by ${prometheusPackageManifest.metadata.labels.provider}`, + `provided by ${prometheusPackageManifest.metadata?.labels?.provider}`, ); expect( prometheusTileProps.description.startsWith( @@ -110,7 +113,7 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] OperatorHubList', () expect(modalItem.imgUrl).toEqual( '/api/kubernetes/apis/packages.operators.coreos.com/v1/namespaces/openshift-operator-lifecycle-manager/packagemanifests/amq-streams/icon?resourceVersion=amq-streams.preview.amqstreams.v1.0.0.beta', ); - expect(modalItem.provider).toEqual(amqPackageManifest.metadata.labels.provider); + expect(modalItem.provider).toEqual(amqPackageManifest.metadata?.labels?.provider); expect( modalItem.description.startsWith( '**Red Hat AMQ Streams** is a massively scalable, distributed, and high performance data streaming platform based on the Apache Kafka project.', @@ -142,7 +145,7 @@ xdescribe(`[https://issues.redhat.com/browse/CONSOLE-2136] ${OperatorHubTileView }); it('updates filter counts on item changes', () => { - wrapper.setProps(operatorHubTileViewPagePropsWithDummy); + wrapper.setProps(operatorHubTileViewPagePropsWithDummy as any); wrapper.update(); const filterItemsChanged = wrapper.find(FilterSidePanelCategoryItem); @@ -169,7 +172,7 @@ xdescribe(`[https://issues.redhat.com/browse/CONSOLE-2136] ${OperatorHubTileView const { filter, resultLength } = filterTest; const results = _.reduce( operatorHubTileViewPageProps.items, - (matches, item) => { + (matches: OperatorHubItem[], item) => { if (keywordCompare(filter, item)) { matches.push(item); } @@ -194,7 +197,7 @@ xdescribe(`[https://issues.redhat.com/browse/CONSOLE-2136] ${OperatorHubTileView // TODO: Test category functionality }); -describe(OperatorHubItemDetails.displayName, () => { +describe(OperatorHubItemDetails.displayName || '', () => { const wrapper: ReactWrapper = mount( , { @@ -211,7 +214,7 @@ describe(OperatorHubItemDetails.displayName, () => { expect(noMarkdown.exists()).toBe(false); - wrapper.setProps({ item: itemWithLongDescription }); + wrapper.setProps({ item: itemWithLongDescription as any }); wrapper.update(); const markdown = wrapper.find(MarkdownView); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx index d7d7671f0fa..75ac1ce7897 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx @@ -80,7 +80,7 @@ describe('PackageManifestTableRow', () => { wrapper = shallow( , ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx index ac0aff906cd..8991f03f64d 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx @@ -115,7 +115,9 @@ describe('SubscriptionTableRow', () => { }; wrapper = updateWrapper(); - expect(wrapper.childAt(2).find(SubscriptionStatus).shallow().text()).toContain('Upgrading'); + expect(wrapper.childAt(2).find(SubscriptionStatus).shallow().text()).toContain( + 'Upgrade available', + ); }); it('renders column for subscription state when no updates available', () => { From aecc829e1a08eefc9b0e906b12e15da24b1950dc Mon Sep 17 00:00:00 2001 From: Krish Agarwal Date: Tue, 26 Aug 2025 14:22:28 -0400 Subject: [PATCH 3/5] Fixed build and test failures --- .../spec/resource-requirements.spec.tsx | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx index c55b4ed5f55..49717a444fb 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx @@ -78,19 +78,12 @@ describe(ResourceRequirementsModalLink.displayName || '', () => { beforeEach(() => { obj = _.cloneDeep(testResourceInstance); - if (obj.spec) { - obj.spec.resources = { + obj.spec = { + resources: { limits: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, requests: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, - }; - } else { - obj.spec = { - resources: { - limits: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, - requests: { memory: '50Mi', cpu: '500m', 'ephemeral-storage': '50Mi' }, - }, - }; - } + }, + }; wrapper = shallow( { }); it('renders default values if undefined', () => { - if (obj.spec) { - obj.spec.resources = {} as any; - } else { - obj.spec = { resources: {} as any }; - } + obj.spec = { resources: {} as any }; wrapper.setProps({ obj }); expect(wrapper.find(Button).render().text()).toEqual('CPU: None, Memory: None, Storage: None'); From 8bf23eeee987c08012ef42cdfbf9d1f7d6b3d7ca Mon Sep 17 00:00:00 2001 From: Krish Agarwal Date: Wed, 3 Sep 2025 14:59:02 -0400 Subject: [PATCH 4/5] Addressed Jon's reviews --- .../user-preferences/UserPreferenceForm.tsx | 2 +- .../user-preferences/UserPreferencePage.tsx | 6 +- .../volume-snapshot-content-details.tsx | 3 +- .../volume-snapshot-content.tsx | 6 +- .../volume-snapshot-details.tsx | 2 +- .../volume-snapshot/volume-snapshot.tsx | 4 +- .../hooks/useExtensionCatalogCategories.ts | 8 +- .../components/clusterserviceversion.spec.tsx | 41 +++---- .../src/components/dashboard/csv-status.tsx | 6 +- .../descriptors/spec/configure-size.tsx | 2 +- .../spec/configure-update-strategy.tsx | 2 +- .../src/components/descriptors/spec/index.tsx | 103 +++++++++--------- .../operand/DEPRECATED_operand-form.tsx | 6 +- .../src/components/operand/index.tsx | 5 +- .../src/components/operand/operand-link.tsx | 3 +- .../operator-hub/operator-hub-items.tsx | 15 ++- .../operator-hub/operator-hub.spec.tsx | 5 +- .../TopologyOperatorBackedResources.tsx | 9 +- .../topology/sidebar/resource-sections.tsx | 2 +- .../src/utils/useClusterServiceVersions.tsx | 2 +- 20 files changed, 118 insertions(+), 114 deletions(-) diff --git a/frontend/packages/console-app/src/components/user-preferences/UserPreferenceForm.tsx b/frontend/packages/console-app/src/components/user-preferences/UserPreferenceForm.tsx index c6500b799b0..3e2d88c41fd 100644 --- a/frontend/packages/console-app/src/components/user-preferences/UserPreferenceForm.tsx +++ b/frontend/packages/console-app/src/components/user-preferences/UserPreferenceForm.tsx @@ -8,7 +8,7 @@ import './UserPreferenceForm.scss'; type UserPreferenceFormProps = { items: ResolvedUserPreferenceItem[] }; const UserPreferenceForm: React.FC = ({ items }) => - items?.length > 0 ? ( + items && items.length > 0 ? (
event.preventDefault()} className="co-user-preference__form"> {items.map((item) => ( diff --git a/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx b/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx index 946048d7f69..6afd40b6fbb 100644 --- a/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx +++ b/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useState } from 'react'; import { Tabs, Tab, @@ -57,8 +58,9 @@ const UserPreferencePage: React.FC = () => { const { group: groupIdFromUrl } = useParams(); const initialTabId = sortedUserPreferenceGroups.find((extension) => extension.id === groupIdFromUrl)?.id || - sortedUserPreferenceGroups[0]?.id; - const [activeTabId, setActiveTabId] = React.useState(initialTabId); + sortedUserPreferenceGroups[0]?.id || + 'general'; + const [activeTabId, setActiveTabId] = useState(initialTabId); const [userPreferenceTabs, userPreferenceTabContents] = React.useMemo< [React.ReactElement[], React.ReactElement[]] diff --git a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content-details.tsx b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content-details.tsx index 93eb74b1e50..33bbbdcb3bd 100644 --- a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content-details.tsx +++ b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content-details.tsx @@ -28,7 +28,8 @@ const { editYaml, events } = navFactory; const Details: React.FC = ({ obj }) => { const { t } = useTranslation(); - const { deletionPolicy, driver } = obj?.spec; + const deletionPolicy = obj?.spec?.deletionPolicy || ''; + const driver = obj?.spec?.driver || ''; const { volumeHandle, snapshotHandle } = obj?.spec?.source || {}; const { name: snapshotName, namespace: snapshotNamespace } = obj?.spec?.volumeSnapshotRef || {}; const size = obj.status?.restoreSize; diff --git a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content.tsx b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content.tsx index f69aaa75fa3..b9d2816c6fd 100644 --- a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content.tsx +++ b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content.tsx @@ -43,8 +43,10 @@ export const tableColumnInfo = [ ]; const Row: React.FC> = ({ obj }) => { - const { name, creationTimestamp } = obj?.metadata || {}; - const { name: snapshotName, namespace: snapshotNamespace } = obj?.spec?.volumeSnapshotRef || {}; + const name = obj?.metadata?.name || ''; + const creationTimestamp = obj?.metadata?.creationTimestamp || ''; + const snapshotName = obj?.spec?.volumeSnapshotRef?.name || ''; + const snapshotNamespace = obj?.spec?.volumeSnapshotRef?.namespace || ''; const size = obj.status?.restoreSize; const sizeMetrics = size ? humanizeBinaryBytes(size).string : '-'; diff --git a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-details.tsx b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-details.tsx index 41c82d2987f..4c9424f0019 100644 --- a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-details.tsx +++ b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-details.tsx @@ -40,7 +40,7 @@ const { editYaml, events } = navFactory; const Details: React.FC = ({ obj }) => { const { t } = useTranslation(); - const { namespace } = obj.metadata || {}; + const namespace = obj?.metadata?.namespace || ''; const sourceModel = obj?.spec?.source?.persistentVolumeClaimName ? PersistentVolumeClaimModel : VolumeSnapshotContentModel; diff --git a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx index df8b0d93475..e1ca3165be7 100644 --- a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx +++ b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx @@ -129,7 +129,9 @@ const Row: React.FC { - const { name, namespace, creationTimestamp } = obj?.metadata || {}; + const name = obj?.metadata?.name || ''; + const namespace = obj?.metadata?.namespace || ''; + const creationTimestamp = obj?.metadata?.creationTimestamp || ''; const size = obj?.status?.restoreSize; const sizeBase = convertToBaseValue(size); const sizeMetrics = size ? humanizeBinaryBytes(sizeBase).string : '-'; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts index 734f2c9c3e5..d5b0d61b792 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts @@ -4,16 +4,16 @@ import { usePoll } from '@console/internal/components/utils'; import { ExtensionCatalogDatabaseContext } from '../contexts/ExtensionCatalogDatabaseContext'; import { getUniqueIndexKeys, openDatabase } from '../database/indexeddb'; -export const useExtensionCatalogCategories = (): [CatalogCategory[], boolean, Error] => { +export const useExtensionCatalogCategories = (): [CatalogCategory[], boolean, string] => { const { done: initDone, error: initError } = React.useContext(ExtensionCatalogDatabaseContext); const [categories, setCategories] = React.useState([]); const [loading, setLoading] = React.useState(!initDone); - const [error, setError] = React.useState(initError ?? null); + const [error, setError] = React.useState(initError.toString() || ''); React.useEffect(() => { if (!initDone || initError) { setLoading(!initDone); - setError(initError); + setError(initError.toString() || ''); } }, [initDone, initError]); @@ -36,7 +36,7 @@ export const useExtensionCatalogCategories = (): [CatalogCategory[], boolean, Er () => categories.map((c) => ({ id: c as string, label: c as string, tags: [c as string] })), [categories], ); - return [catalogCategories, loading, error ?? new Error('Unknown error')]; + return [catalogCategories, loading, error]; }; export default useExtensionCatalogCategories; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx index d54e85679e3..7dfee5675ca 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx @@ -248,13 +248,25 @@ describe(ClusterServiceVersionList.displayName || '', () => { }); describe(CRDCard.displayName || '', () => { - const crd = testClusterServiceVersion.spec.customresourcedefinitions?.owned?.[0]; + const testCRD = { + name: 'test.example.com', + kind: 'Test', + version: 'v1', + displayName: 'Test Resource', + }; + + const testCSVWithCRD = { + ...testClusterServiceVersion, + spec: { + ...testClusterServiceVersion.spec, + customresourcedefinitions: { + owned: [testCRD], + }, + }, + }; it('renders a card with title, body, and footer', () => { - if (!crd) { - throw new Error('Test CRD is undefined'); - } - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(CardTitle).exists()).toBe(true); expect(wrapper.find(CardBody).exists()).toBe(true); @@ -262,26 +274,17 @@ describe(CRDCard.displayName || '', () => { }); it('renders a link to create a new instance', () => { - // Ensure crd is defined before passing to CRDCard to avoid type errors - if (!crd) { - throw new Error('Test CRD is undefined'); - } - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(CardFooter).find(ReactRouter.Link).props().to).toEqual( - `/k8s/ns/${testClusterServiceVersion.metadata?.namespace}/${ - ClusterServiceVersionModel.plural - }/${testClusterServiceVersion.metadata?.name}/${referenceForProvidedAPI(crd)}/~new`, + `/k8s/ns/${testCSVWithCRD.metadata?.namespace}/${ClusterServiceVersionModel.plural}/${ + testCSVWithCRD.metadata?.name + }/${referenceForProvidedAPI(testCRD)}/~new`, ); }); it('does not render link to create new instance if `props.canCreate` is false', () => { - if (!crd) { - throw new Error('Test CRD is undefined'); - } - const wrapper = shallow( - , - ); + const wrapper = shallow(); expect(wrapper.find(CardFooter).find(ReactRouter.Link).exists()).toBe(false); }); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx index cd8ac2caf0d..d9289a5d999 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { pluralize } from '@patternfly/react-core'; -import { Link } from 'react-router-dom-v5-compat'; +import { Link, To } from 'react-router-dom-v5-compat'; import { resourcePathFromModel, resourcePath, @@ -18,7 +18,7 @@ const ClusterServiceVersionRow: React.FC { const { name, namespace } = operatorStatus.operators[0]?.metadata || {}; const { displayName } = operatorStatus.operators[0]?.spec || {}; - const to = + const to: To = operatorStatus.operators.length > 1 ? `${resourcePathFromModel(ClusterServiceVersionModel)}?name=${name}` : resourcePath(referenceForModel(ClusterServiceVersionModel), name, namespace); @@ -28,7 +28,7 @@ const ClusterServiceVersionRow: React.FC - + {displayName || name} diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-size.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-size.tsx index dec464883c8..f7cd81fc067 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-size.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-size.tsx @@ -24,7 +24,7 @@ export const configureSizeModal = ({ }); }; -type ConfigureSizeModalProps = { +export type ConfigureSizeModalProps = { kindObj: K8sKind; resource: K8sResourceKind; specDescriptor: Descriptor; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-update-strategy.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-update-strategy.tsx index 9d70009afff..a1c9692dc45 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-update-strategy.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-update-strategy.tsx @@ -19,7 +19,7 @@ export const configureUpdateStrategyModal = ({ }); }; -type ConfigureUpdateStrategyModalProps = { +export type ConfigureUpdateStrategyModalProps = { kindObj: K8sKind; resource: K8sResourceKind; specDescriptor: Descriptor; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx index 87b67868452..5c83b09c708 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/index.tsx @@ -18,19 +18,16 @@ import { LabelList, LabelListProps, } from '@console/internal/components/utils'; -import { - K8sKind, - K8sResourceKind, - k8sPatch, - k8sUpdate, - Selector as SelectorType, -} from '@console/internal/module/k8s'; +import { k8sPatch, k8sUpdate, Selector as SelectorType } from '@console/internal/module/k8s'; import { YellowExclamationTriangleIcon } from '@console/shared'; import { DefaultCapability, K8sResourceLinkCapability, SecretCapability } from '../common'; -import { CapabilityProps, Descriptor, SpecCapability, Error } from '../types'; +import { CapabilityProps, SpecCapability, Error } from '../types'; import { getPatchPathFromDescriptor, getValidCapabilitiesForValue } from '../utils'; -import { configureSizeModal } from './configure-size'; -import { configureUpdateStrategyModal } from './configure-update-strategy'; +import { configureSizeModal, ConfigureSizeModalProps } from './configure-size'; +import { + configureUpdateStrategyModal, + ConfigureUpdateStrategyModalProps, +} from './configure-update-strategy'; import { EndpointList, EndpointListProps } from './endpoint'; import { ResourceRequirementsModalLink } from './resource-requirements'; @@ -42,24 +39,29 @@ const PodCount: React.FC> = ({ obj, fullPath, value, -}) => ( - - configureSizeModal({ - kindObj: model as K8sKind, - resource: obj as K8sResourceKind, - specDescriptor: descriptor as Descriptor, - specValue: value, - }) - } - > - {_.isNil(value) ? '-' : `${value} pods`} - -); +}) => { + const sizeModalProps = React.useMemo( + () => ({ + kindObj: model, + resource: obj, + specDescriptor: descriptor, + specValue: value, + }), + [model, obj, descriptor, value], + ); + + return ( + configureSizeModal(sizeModalProps)} + > + {_.isNil(value) ? '-' : `${value} pods`} + + ); +}; const Endpoints: React.FC> = ({ description, @@ -133,21 +135,13 @@ const ResourceRequirements: React.FC = ({ {t('olm~Resource limits')} - + {t('olm~Resource requests')} - + @@ -189,7 +183,7 @@ const BooleanSwitch: React.FC> = ({ const { t } = useTranslation(); const [checked, setChecked] = React.useState(Boolean(value)); const [confirmed, setConfirmed] = React.useState(false); - const [errorMessage, setErrorMessage] = React.useState(null); + const [errorMessage, setErrorMessage] = React.useState(''); const errorCb = (err: Error): void => { setConfirmed(false); @@ -200,22 +194,20 @@ const BooleanSwitch: React.FC> = ({ const update = (): void => { setConfirmed(true); - setErrorMessage(null); + setErrorMessage(''); if (_.has(obj, `spec.${descriptor.path}`)) { const patchFor = (val: boolean) => [ { op: 'add', path: `/spec/${getPatchPathFromDescriptor(descriptor)}`, value: val }, ]; - k8sPatch(model as K8sKind, obj as K8sResourceKind, patchFor(checked)).catch((err) => - errorCb(err), - ); + k8sPatch(model, obj, patchFor(checked)).catch((err) => errorCb(err)); return; } const newObj = _.cloneDeep(obj); if (newObj) { _.set(newObj, `spec.${descriptor.path}`, checked); - k8sUpdate(model as K8sKind, newObj as K8sResourceKind).catch((err) => errorCb(err)); + k8sUpdate(model, newObj).catch((err) => errorCb(err)); } }; @@ -233,7 +225,7 @@ const BooleanSwitch: React.FC> = ({ onChange={(_event, val): void => { setChecked(val); setConfirmed(false); - setErrorMessage(null); + setErrorMessage(''); }} label={t('public~True')} /> @@ -276,7 +268,7 @@ const CheckboxUIComponent: React.FC> = ({ ]; const update = (): void => { setConfirmed(true); - k8sPatch(model as K8sKind, obj as K8sResourceKind, patchFor(checked)); + k8sPatch(model, obj, patchFor(checked)); }; return ( @@ -325,19 +317,22 @@ const UpdateStrategy: React.FC = ({ value, }) => { const { t } = useTranslation(); + const updateStrategyModalProps = React.useMemo( + () => ({ + kindObj: model, + resource: obj, + specDescriptor: descriptor, + specValue: value, + }), + [model, obj, descriptor, value], + ); + return ( - configureUpdateStrategyModal({ - kindObj: model as K8sKind, - resource: obj as K8sResourceKind, - specDescriptor: descriptor as Descriptor, - specValue: value, - }) - } + onEdit={() => configureUpdateStrategyModal(updateStrategyModalProps)} path={fullPath} > {value?.type ?? t('public~None')} diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx index d7a357ba98c..5516cdc915b 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx @@ -117,11 +117,7 @@ const idFromPath = (path) => `DEPRECATED_root_${path.split('.').join('_')}`; * both prefix and suffix are provided, this will return true only if the field has a capability * that matches the concatenation of prefix + suffix. */ -const hasDescriptor = ( - field: OperandField, - prefix: string, - suffix: string | null = null, -): boolean => { +const hasDescriptor = (field: OperandField, prefix: string, suffix: string = ''): boolean => { return suffix ? _.includes(field.capabilities, `${prefix}${suffix}`) : _.some(field.capabilities, (capability) => capability.startsWith(prefix)); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx index 10a81c76fb0..44db76af20a 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx @@ -147,12 +147,11 @@ const hasAllNamespaces = (csv: ClusterServiceVersionKind) => { }; export const OperandStatus: React.FC = ({ operand }) => { - const status: OperandStatusType | null = getOperandStatus(operand); - if (!status) { + const { type, value }: OperandStatusType = getOperandStatus(operand); + if (!type || !value) { return <>-; } - const { type, value } = status; return ( {type} diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx index 83477bbf415..be641ca71b7 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-link.tsx @@ -15,7 +15,8 @@ export const csvNameFromWindow = () => ); export const OperandLink: React.FC = (props) => { - const { namespace, name } = props.obj.metadata || { namespace: '', name: '' }; + const namespace = props.obj.metadata?.namespace || ''; + const name = props.obj.metadata?.name || ''; const csvName = props.csvName || csvNameFromWindow(); const reference = referenceFor(props.obj); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx index eac9cf7cea2..f7eb93896af 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx @@ -571,14 +571,14 @@ const OperatorHubTile: React.FC = ({ item, onClick }) => { export const OperatorHubTileView: React.FC = (props) => { const { t } = useTranslation(); - const [detailsItem, setDetailsItem] = React.useState(null); + const [detailsItem, setDetailsItem] = React.useState(); const [showDetails, setShowDetails] = React.useState(false); const [ignoreOperatorWarning, setIgnoreOperatorWarning, loaded] = useUserSettingsCompatibility< boolean >(userSettingsKey, storeKey, false); const [updateChannel, setUpdateChannel] = React.useState(''); const [updateVersion, setUpdateVersion] = React.useState(''); - const [tokenizedAuth, setTokenizedAuth] = React.useState(null); + const [tokenizedAuth, setTokenizedAuth] = React.useState(''); const installVersion = getQueryArgument('version'); const filteredItems = filterByArchAndOS(props.items); @@ -813,10 +813,13 @@ export const OperatorHubTileView: React.FC = (props) = const installLink = detailsItem && detailsItem.obj && `/operatorhub/subscribe?${installParamsURL}`; - const uninstallLink = () => - detailsItem && - detailsItem.subscription && - `/k8s/ns/${detailsItem.subscription.metadata?.namespace}/${SubscriptionModel.plural}/${detailsItem.subscription.metadata?.name}?showDelete=true`; + const uninstallLink = () => { + const subscriptionName = detailsItem?.subscription?.metadata?.name; + const subscriptionNamespace = detailsItem?.subscription?.metadata?.namespace; + return subscriptionName && subscriptionNamespace + ? `/k8s/ns/${subscriptionNamespace}/${SubscriptionModel.plural}/${subscriptionName}?showDelete=true` + : ''; + }; if (_.isEmpty(filteredItems)) { return ( diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx index 6c20af3b6c3..7231bb47c32 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx @@ -40,7 +40,6 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] OperatorHubList', () {...operatorHubListPageProps} marketplacePackageManifests={null as any} subscriptions={{ loaded: false, data: [] }} - loadError={undefined} clusterServiceVersions={{ loaded: false, data: [] }} /> , @@ -172,13 +171,13 @@ xdescribe(`[https://issues.redhat.com/browse/CONSOLE-2136] ${OperatorHubTileView const { filter, resultLength } = filterTest; const results = _.reduce( operatorHubTileViewPageProps.items, - (matches: OperatorHubItem[], item) => { + (matches: OperatorHubItem[], item: OperatorHubItem) => { if (keywordCompare(filter, item)) { matches.push(item); } return matches; }, - [], + [] as OperatorHubItem[], ); expect(results.length).toBe(resultLength); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx b/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx index 2785c121dc1..f41f959ef7d 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/TopologyOperatorBackedResources.tsx @@ -79,7 +79,8 @@ const OperatorResourcesGetter: React.FC = ({ }) => { const providedAPI = providedAPIForReference(csv, modelReference); const linkForResource = (obj: K8sResourceKind) => { - return linkForCsvResource(obj, providedAPI as ProvidedAPI, csv.metadata?.name || ''); + const csvName = csv.metadata?.name || ''; + return linkForCsvResource(obj, providedAPI as ProvidedAPI, csvName); }; const defaultResources = [ 'Deployment', @@ -134,13 +135,13 @@ const TopologyOperatorBackedResources: React.FC { const { t } = useTranslation(); const { resource } = item; - const { namespace } = resource?.metadata || {}; + const namespace = resource?.metadata?.namespace || ''; const reference = referenceFor(resource as K8sResourceCommon); const flatten = flattenCsvResources(resource as K8sResourceCommon); const getManagedByCSVResourceLink = () => { const model = modelFor(referenceFor(csv)); - const { name } = csv.metadata || {}; - const { kind } = model || {}; + const name = csv.metadata?.name || ''; + const kind = model?.kind || ''; const link = `/k8s/ns/${namespace}/${referenceForModel(ClusterServiceVersionModel)}/${name}`; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/resource-sections.tsx b/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/resource-sections.tsx index 7acbdb4f5e1..e944b0eecbe 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/resource-sections.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/topology/sidebar/resource-sections.tsx @@ -13,7 +13,7 @@ import { OperatorGroupData } from './types'; const ResourceSection: React.FC<{ item: TopologyDataObject }> = ({ item }) => { const { resource, data } = item; - const { namespace } = resource?.metadata || {}; + const namespace = resource?.metadata?.namespace ?? ''; const { csvName } = data; const resourcesList = React.useMemo(() => { diff --git a/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersions.tsx b/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersions.tsx index 4df8c195455..4111a341269 100644 --- a/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersions.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/utils/useClusterServiceVersions.tsx @@ -121,7 +121,7 @@ const normalizeClusterServiceVersions = ( operatorName, }, icon: { - class: undefined, + class: '', url: getImageForCSVIcon(desc.csv.spec.icon?.[0]), }, cta: { From ac3097a78e7defe11f307c5b7c3fdb16523d74b2 Mon Sep 17 00:00:00 2001 From: Krish Agarwal Date: Fri, 5 Sep 2025 10:10:07 -0400 Subject: [PATCH 5/5] Hardened types and replaced instances of 'any' --- .../src/contexts/types.ts | 1 + ...seExtensionCatalogDatabaseContextValues.ts | 1 + .../src/database/injest.ts | 1 + .../src/database/jsonl.ts | 1 + .../src/database/types.ts | 1 + .../src/fbc/bundles.ts | 1 + .../src/fbc/catalog-item.tsx | 1 + .../src/fbc/metadata.ts | 1 + .../src/fbc/packages.ts | 1 + .../src/fbc/types.ts | 1 + .../hooks/useExtensionCatalogCategories.ts | 1 + .../src/hooks/useExtensionCatalogItems.ts | 1 + .../operator-lifecycle-manager/mocks.ts | 6 + .../src/components/dashboard/csv-status.tsx | 5 +- .../src/components/dashboard/utils.ts | 9 +- .../src/components/descriptors/index.tsx | 47 +++-- .../descriptors/spec/configure-size.tsx | 2 +- .../spec/configure-update-strategy.tsx | 2 +- .../spec/resource-requirements.spec.tsx | 4 +- .../modals/delete-catalog-source-modal.tsx | 4 +- .../modals/disable-default-source-modal.tsx | 4 +- .../modals/edit-default-sources-modal.tsx | 4 +- .../installplan-approval-modal.spec.tsx | 10 +- .../subscription-channel-modal.spec.tsx | 2 +- .../modals/subscription-channel-modal.tsx | 2 +- .../modals/uninstall-operator-modal.tsx | 25 ++- .../modals/update-strategy-modal.tsx | 4 +- .../operand/DEPRECATED_operand-form.tsx | 50 +++-- .../src/components/operand/create-operand.tsx | 4 +- .../src/components/operand/index.tsx | 48 +++-- .../src/components/operand/operand-form.tsx | 6 +- .../src/components/operator-hub/index.ts | 7 +- .../operator-hub-item-details.tsx | 10 +- .../operator-hub/operator-hub-items.tsx | 171 ++++++++++++------ .../operator-hub/operator-hub-utils.spec.ts | 8 +- .../operator-hub/operator-hub.spec.tsx | 12 +- .../src/components/package-manifest.spec.tsx | 6 +- .../src/components/subscription.spec.tsx | 3 +- .../operator-lifecycle-manager/src/types.ts | 2 +- frontend/public/components/factory/modal.tsx | 10 +- 40 files changed, 306 insertions(+), 173 deletions(-) diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts b/frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts index 1c41521af21..a53839ebbaf 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/contexts/types.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 export type ExtensionCatalogDatabaseContextValues = { done: boolean; error: Error | null; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts b/frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts index a92fe81740e..bc54b5224c3 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/contexts/useExtensionCatalogDatabaseContextValues.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 import { useEffect, useRef, useState } from 'react'; import * as _ from 'lodash'; import { K8sResourceCommon } from '@console/dynamic-plugin-sdk/src'; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts b/frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts index 1c8a690b185..3549b0bdd81 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/database/injest.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 /* eslint-disable no-console */ import { K8sResourceCommon } from '@console/dynamic-plugin-sdk/src/lib-core'; import { bundleHasProperty } from '../fbc/bundles'; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts b/frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts index 73bbcfcf676..d5c9c80d99c 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/database/jsonl.ts @@ -31,6 +31,7 @@ export const parseJSONLines = () => }, }); +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 // ObjectType is the expected shape of each JSON object (defaults to any). // HandlerResult is the expected value the handler will resolve when called with ObjectType as an argument export const fetchAndProcessJSONLines = ( diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/database/types.ts b/frontend/packages/operator-lifecycle-manager-v1/src/database/types.ts index 956a85cb87c..10524768fc5 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/database/types.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/database/types.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 import { InfrastructureFeature } from '@console/operator-lifecycle-manager/src/components/operator-hub'; export enum FileBasedCatalogSchema { diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts index cdd0ae48a7e..68870907c6a 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/bundles.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 import * as SemVer from 'semver'; import { getIndexedItems } from '../database/indexeddb'; import { getBundleMetadata } from './metadata'; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx index b31418ccdb9..e659f905b8e 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx +++ b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/catalog-item.tsx @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 import { CatalogItem } from '@console/dynamic-plugin-sdk/src'; import { SyncMarkdownView } from '@console/internal/components/markdown-view'; import { CapabilityLevel } from '@console/operator-lifecycle-manager/src/components/operator-hub/operator-hub-item-details'; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts index 23b778207d3..1b1163bf824 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/metadata.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 /* eslint-disable no-console */ import * as _ from 'lodash'; import { diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts index f38b4806bc8..e2ea4283e8f 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/packages.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 /* eslint-disable no-console */ import { defaultClusterCatalogSourceMap } from '@console/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils'; import { PackageSource } from '@console/operator-lifecycle-manager/src/const'; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/types.ts b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/types.ts index 584f4d6b91d..41a404d2e8a 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/fbc/types.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/fbc/types.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 import { InfrastructureFeature, OLMAnnotation, diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts index d5b0d61b792..28e95fb62eb 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogCategories.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 import * as React from 'react'; import { CatalogCategory } from '@console/dynamic-plugin-sdk/src'; import { usePoll } from '@console/internal/components/utils'; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts index fae0adc729d..15d065379d5 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useExtensionCatalogItems.ts @@ -1,3 +1,4 @@ +// This file will be removed as part of https://issues.redhat.com//browse/CONSOLE-4668 import * as React from 'react'; import { CatalogItem } from '@console/dynamic-plugin-sdk/src'; import { usePoll } from '@console/internal/components/utils'; diff --git a/frontend/packages/operator-lifecycle-manager/mocks.ts b/frontend/packages/operator-lifecycle-manager/mocks.ts index 160d8d49d42..5dc282fbec4 100644 --- a/frontend/packages/operator-lifecycle-manager/mocks.ts +++ b/frontend/packages/operator-lifecycle-manager/mocks.ts @@ -781,6 +781,7 @@ export const operatorHubTileViewPageProps = { obj: amqPackageManifest, installState: 'Installed', installed: false, + isInstalling: false, kind: 'PackageManifest', name: 'amq-streams', uid: 'amq-streams/openshift-operator-lifecycle-manager', @@ -812,6 +813,7 @@ export const operatorHubTileViewPageProps = { obj: etcdPackageManifest, installState: 'Not Installed', installed: false, + isInstalling: false, kind: 'PackageManifest', name: 'etcd', uid: 'etcd/openshift-operator-lifecycle-manager', @@ -842,6 +844,7 @@ export const operatorHubTileViewPageProps = { obj: federationv2PackageManifest, installState: 'Not Installed', installed: false, + isInstalling: false, kind: 'PackageManifest', name: 'federationv2', uid: 'federationv2/openshift-operator-lifecycle-manager', @@ -872,6 +875,7 @@ export const operatorHubTileViewPageProps = { obj: prometheusPackageManifest, installState: 'Not Installed', installed: false, + isInstalling: false, kind: 'PackageManifest', name: 'prometheus', uid: 'prometheus/openshift-operator-lifecycle-manager', @@ -911,6 +915,7 @@ export const operatorHubTileViewPagePropsWithDummy = { { obj: dummyPackageManifest, installed: false, + isInstalling: false, kind: 'PackageManifest', name: 'dummy', uid: 'dummy/openshift-operator-lifecycle-manager', @@ -1032,6 +1037,7 @@ export const itemWithLongDescription = { obj: amqPackageManifest, kind: 'PackageManifest', installed: false, + isInstalling: false, name: 'amq-streams', uid: 'amq-streams/openshift-operator-lifecycle-manager', iconClass: null, diff --git a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx index d9289a5d999..900d342eb2b 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/csv-status.tsx @@ -16,8 +16,9 @@ import './csv-status.scss'; const ClusterServiceVersionRow: React.FC> = ({ operatorStatus, }) => { - const { name, namespace } = operatorStatus.operators[0]?.metadata || {}; - const { displayName } = operatorStatus.operators[0]?.spec || {}; + const name = operatorStatus.operators[0]?.metadata?.name || ''; + const namespace = operatorStatus.operators[0]?.metadata?.namespace || ''; + const displayName = operatorStatus.operators[0]?.spec?.displayName; const to: To = operatorStatus.operators.length > 1 ? `${resourcePathFromModel(ClusterServiceVersionModel)}?name=${name}` diff --git a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/utils.ts b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/utils.ts index 6044009ec67..f69ee160d3e 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/dashboard/utils.ts +++ b/frontend/packages/operator-lifecycle-manager/src/components/dashboard/utils.ts @@ -35,23 +35,26 @@ const getOperatorStatus = ( operatorHealth !== HealthState.ERROR && subscriptionStatus.status === SubscriptionState.SubscriptionStateUpgradePending ) { + const subscriptionTitle = subscriptionStatus.title || ''; return { ...healthStateMapping[HealthState.UPDATING], - title: subscriptionStatus.title || '', + title: subscriptionTitle, }; } if ( operatorHealth !== HealthState.ERROR && subscriptionStatus.status === SubscriptionState.SubscriptionStateUpgradeAvailable ) { + const subscriptionTitle = subscriptionStatus.title || ''; return { ...healthStateMapping[HealthState.UPGRADABLE], - title: subscriptionStatus.title || '', + title: subscriptionTitle, }; } + const csvTitle = csvStatus.title || ''; return { ...healthStateMapping[operatorHealth], - title: csvStatus.title || '', + title: csvTitle, }; }; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/index.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/index.tsx index 25c184f29fe..05e27cd3094 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/index.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/index.tsx @@ -60,10 +60,9 @@ const DescriptorDetailsItemArrayGroup: React.FC const { arrayGroupPath, elementDescriptor, descriptor, nested } = group; const arrayGroupSchema = getSchemaAtPath(schema, `${type}.${arrayGroupPath}`); const description = descriptor?.description || arrayGroupSchema?.description; - const label = - descriptor?.displayName || - arrayGroupSchema?.title || - _.startCase(_.last(arrayGroupPath?.split('.') || [])); + const arrayGroupPathArray = arrayGroupPath?.split('.') || []; + const lastPathSegment = _.last(arrayGroupPathArray); + const label = descriptor?.displayName || arrayGroupSchema?.title || _.startCase(lastPathSegment); const arrayElementDescriptors = nested ?? [elementDescriptor]; const value = _.get(obj, [type, ..._.toPath(arrayGroupPath)], []); return ( @@ -108,26 +107,34 @@ const DescriptorDetailsItemGroup: React.FC = ({ const { descriptor, nested } = group; const groupSchema = getSchemaAtPath(schema, `${type}.${groupPath}`); const description = descriptor?.description || groupSchema?.description; - const label = descriptor?.displayName || groupSchema?.title || _.startCase(groupPath); + const groupDisplayName = descriptor?.displayName || groupSchema?.title; + const label = groupDisplayName || _.startCase(groupPath); const arrayGroups = _.pickBy(nested, 'isArrayGroup'); const primitives = _.omitBy(nested, 'isArrayGroup'); - const span = _.isEmpty(arrayGroups) || _.isEmpty(primitives) ? 6 : 12; + const hasOnlyArrayGroups = _.isEmpty(primitives); + const hasOnlyPrimitives = _.isEmpty(arrayGroups); + const span = hasOnlyArrayGroups || hasOnlyPrimitives ? 6 : 12; return ( {!_.isEmpty(primitives) && - _.map(primitives, ({ descriptor: primitiveDescriptor }: DescriptorGroup) => ( - - ))} + _.map(primitives, ({ descriptor: primitiveDescriptor }: DescriptorGroup) => { + if (!primitiveDescriptor) { + return null; + } + return ( + + ); + })} {!_.isEmpty(arrayGroups) && _.map(arrayGroups, (arrayGroup: DescriptorGroup) => ( ); } + if (!descriptor) { + return null; + } + return ( ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-size.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-size.tsx index f7cd81fc067..dbbe39f341e 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-size.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-size.tsx @@ -28,5 +28,5 @@ export type ConfigureSizeModalProps = { kindObj: K8sKind; resource: K8sResourceKind; specDescriptor: Descriptor; - specValue: any; + specValue: unknown; }; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-update-strategy.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-update-strategy.tsx index a1c9692dc45..3373f924e77 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-update-strategy.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/configure-update-strategy.tsx @@ -23,5 +23,5 @@ export type ConfigureUpdateStrategyModalProps = { kindObj: K8sKind; resource: K8sResourceKind; specDescriptor: Descriptor; - specValue: any; + specValue: unknown; }; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx index 49717a444fb..51d447e436d 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/descriptors/spec/resource-requirements.spec.tsx @@ -19,7 +19,7 @@ describe(ResourceRequirementsModal.name, () => { const description = 'Define the resource requests for this TestResource instance.'; const cancel = jasmine.createSpy('cancelSpy'); - const spyAndExpect = (spy: Spy) => (returnValue: any) => + const spyAndExpect = (spy: Spy) => (returnValue: unknown) => new Promise((resolve) => spy.and.callFake((...args) => { resolve(args); @@ -111,7 +111,7 @@ describe(ResourceRequirementsModalLink.displayName || '', () => { }); it('renders default values if undefined', () => { - obj.spec = { resources: {} as any }; + obj.spec = { resources: {} }; wrapper.setProps({ obj }); expect(wrapper.find(Button).render().text()).toEqual('CPU: None, Memory: None, Storage: None'); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/delete-catalog-source-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/delete-catalog-source-modal.tsx index 310153486ba..b1b6ff78b51 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/delete-catalog-source-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/delete-catalog-source-modal.tsx @@ -29,7 +29,7 @@ const DeleteCatalogSourceModal: React.FC = ({ event.preventDefault(); handlePromise(k8sKill(kind, resource)) .then(() => { - close(); + close?.(); }) .catch(() => {}); }, @@ -66,7 +66,7 @@ const DeleteCatalogSourceModal: React.FC = ({ = ({ ]; handlePromise(k8sPatch(kind, operatorHub, patch)) .then(() => { - close(); + close?.(); }) .catch(() => {}); }, @@ -61,7 +61,7 @@ const DisableDefaultSourceModal: React.FC = ({ = ({ ]; handlePromise(k8sPatch(OperatorHubModel, operatorHub, patch)) .then(() => { - close(); + close?.(); }) .catch(() => {}); }, @@ -104,7 +104,7 @@ const EditDefaultSourcesModal: React.FC = ({ errorMessage={errorMessage} inProgress={inProgress} submitText={t('public~Save')} - cancel={cancel as any} + cancel={cancel} /> diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx index 5f87cff54d8..8b25b9675c1 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx @@ -70,7 +70,10 @@ describe(InstallPlanApprovalModal.name, () => { .find(Radio) .at(1) .props() - .onChange?.({ target: { value: InstallPlanApproval.Manual } } as any, true); + .onChange?.( + { target: { value: InstallPlanApproval.Manual } } as React.ChangeEvent, + true, + ); wrapper.find('form').simulate('submit', new Event('submit')); }); @@ -87,7 +90,10 @@ describe(InstallPlanApprovalModal.name, () => { .find(Radio) .at(1) .props() - .onChange?.({ target: { value: InstallPlanApproval.Manual } } as any, true); + .onChange?.( + { target: { value: InstallPlanApproval.Manual } } as React.ChangeEvent, + true, + ); wrapper.find('form').simulate('submit', new Event('submit')); }); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx index e47d183a1af..71352a214fe 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx @@ -91,7 +91,7 @@ describe('SubscriptionChannelModal', () => { .find(Radio) .at(1) .props() - .onChange?.({ target: { value: 'nightly' } } as any, true); + .onChange?.({ target: { value: 'nightly' } } as React.ChangeEvent, true); wrapper.find('form').simulate('submit', new Event('submit')); }); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.tsx index 6390ead54d9..a93c9e3c817 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.tsx @@ -87,7 +87,7 @@ export const SubscriptionChannelModal: React.FC = diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx index 0d8c2114815..11d74f0f906 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx @@ -177,8 +177,8 @@ export const UninstallOperatorModal: React.FC = ({ ), ] : []), - ...(removePlugins - ? [k8sPatch(ConsoleOperatorConfigModel, consoleOperatorConfig, [patch as any])] + ...(removePlugins && patch + ? [k8sPatch(ConsoleOperatorConfigModel, consoleOperatorConfig, [patch])] : []), ]; @@ -343,7 +343,7 @@ export const UninstallOperatorModal: React.FC = ({ operands={operands} loaded={operandsLoaded} csvName={csvName || ''} - cancel={cancel as any} // for breadcrumbs & cancel modal when clicking on operand links + cancel={cancel} // for breadcrumbs & cancel modal when clicking on operand links /> = ({ ) : operandDeletionVerificationError ? ( = ({ = ({ operands, loaded, csvName .map((operand) => ( - + = ({ operandErrors, csvN key={operandError.operand.metadata?.uid || ''} className="pf-v6-c-list pf-m-plain co-operator-uninstall-alert__list-item" > - {' '} + {' '} {operandError.operand.kind} {' '} {t('olm~Error: {{error}}', { @@ -684,13 +684,18 @@ type UninstallOperatorModalProviderProps = UninstallOperatorModalProps & ModalCo export type UninstallOperatorModalProps = { cancel?: () => void; close?: () => void; - k8sKill: (kind: K8sKind, resource: K8sResourceKind, options: any, json: any) => Promise; + k8sKill: ( + kind: K8sKind, + resource: K8sResourceKind, + options: unknown, + json: unknown, + ) => Promise; k8sGet: (kind: K8sKind, name: string, namespace: string) => Promise; k8sPatch: ( kind: K8sKind, resource: K8sResourceKind, - data: { op: string; path: string; value: any }[], - ) => Promise; + data: { op: string; path: string; value: unknown }[], + ) => Promise; subscription: K8sResourceKind; csv?: K8sResourceKind; blocking?: boolean; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx index f21966f68db..9595861d3e4 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/update-strategy-modal.tsx @@ -77,7 +77,7 @@ export const UpdateStrategyModal: React.FC = ({ errorMessage={errorMessage} inProgress={inProgress} submitText={t('public~Save')} - cancel={cancel as any} + cancel={cancel} /> ); @@ -88,7 +88,7 @@ export const updateStrategyModal = createModalLauncher(UpdateStrategyModal); UpdateStrategyModal.displayName = 'UpdateStrategyModal'; export type UpdateStrategyModalProps = { - defaultValue: any; + defaultValue; path: string; resource: K8sResourceKind; resourceKind: K8sKind; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx index 5516cdc915b..595ca448990 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/DEPRECATED_operand-form.tsx @@ -162,12 +162,12 @@ const parseArrayPath = ( */ const modifyArrayFieldPathIndex = ( path: string, - operation: (index?: number) => string | number, + operation: (index: number) => string | number, ): string => { const { regexMatch, index, pathBeforeIndex, pathAfterIndex } = parseArrayPath(path); return !regexMatch ? path - : `${pathBeforeIndex}[${operation(index)}]${pathAfterIndex && `.${pathAfterIndex}`}`; + : `${pathBeforeIndex}[${operation(index || 0)}]${pathAfterIndex && `.${pathAfterIndex}`}`; }; // Accepts a SpecCapbability[] array and returns an appropriate default value for that field @@ -268,7 +268,7 @@ const flattenNestedProperties = ( property: JSONSchema6, name: string, providedAPI: ProvidedAPI, - obj: K8sResourceKind, + obj: K8sResourceKind | Record, { currentCapabilities = [], currentPath = [], @@ -392,7 +392,7 @@ const getPropertyDepth = (property: JSONSchema6, depth: number = 0): number => { const fieldsForOpenAPI = ( schema: JSONSchema6, providedAPI: ProvidedAPI, - obj: K8sResourceKind, + obj: K8sResourceKind | Record, depth: number = MAX_DEPTH, ): OperandField[] => { return _.reduce( @@ -418,7 +418,7 @@ const fieldsForOpenAPI = ( */ const specDescriptorToFields = ( { description, displayName, path, 'x-descriptors': capabilities = [] }: Descriptor, - obj: K8sResourceKind, + obj: K8sResourceKind | Record, ): OperandField[] => { // Use regex to check path for an array index, and parse out the parts of the path before // and after the array index. @@ -435,8 +435,8 @@ const specDescriptorToFields = ( displayName, description, capabilities, - type: null as any, - required: null as any, + type: 'string' as JSONSchema6TypeName, + required: false, })), ); } @@ -445,8 +445,8 @@ const specDescriptorToFields = ( path: `spec.${path}`, displayName, description, - type: null as any, - required: null as any, + type: 'string' as JSONSchema6TypeName, + required: false, capabilities, }, ]; @@ -514,7 +514,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ const { t } = useTranslation(); const params = useParams(); const immutableFormData = Immutable.fromJS(formData); - const handleFormDataUpdate = (path: string, value: any): void => { + const handleFormDataUpdate = (path: string, value: unknown): void => { const { regexMatch, index, pathBeforeIndex, pathAfterIndex } = parseArrayPath(path); // Immutable will not initialize a deep path as a List if it includes an integer, so we need to manually @@ -538,7 +538,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ const schemaFields = fieldsForOpenAPI( schema?.properties?.spec as JSONSchema6, providedAPI, - formData as K8sResourceKind, + formData || {}, ); // Get fields from providedAPI that do not exist in the OpenAPI spec. @@ -553,7 +553,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ // Add the field if it doesn't exist return [ ...providedAPIFieldsAccumulator, - ...specDescriptorToFields(specDescriptor, formData as K8sResourceKind), + ...specDescriptorToFields(specDescriptor, formData || {}), ]; }, [], @@ -641,9 +641,9 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ // of the appropriate fields, grouped by index. return _.map(groupedByName, (fieldsInGroup, groupName: string) => ({ groupName, - fieldLists: _.reduce( + fieldLists: _.reduce( fieldsInGroup, - (fieldListsAccumulator: OperandField[][], field) => { + (fieldListsAccumulator, field) => { const { index, regexMatch } = parseArrayPath(field.path); if (regexMatch) { fieldListsAccumulator[index || 0] = [ @@ -653,7 +653,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ } return fieldListsAccumulator; }, - [] as OperandField[][], + [], ), })); }, [arrayFields]); @@ -816,7 +816,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ desc={displayName} placeholder={t('olm~Select {{item}}', { item: kindForReference(groupVersionKind) })} onChange={(value) => handleFormDataUpdate(path, value)} - selectedKey={currentValue ? `${currentValue}-${k8sModel?.kind}` : undefined} + selectedKey={currentValue ? `${currentValue}-${k8sModel?.kind}` : ''} /> ) : null; } @@ -985,7 +985,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ const leftShiftedFields = _.reduce( fieldsToLeftShift, (fieldAccumulator, field) => { - const path = modifyArrayFieldPathIndex(field.path, (index) => (index || 1) - 1); + const path = modifyArrayFieldPathIndex(field.path, (index) => Math.max(0, index - 1)); return [...fieldAccumulator, { ...field, path }]; }, [], @@ -1018,7 +1018,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ const isExpanded = !_.some(fieldLists, (fieldList: OperandField[]) => _.some( fieldList, - (f) => hasDescriptor(f as OperandField, SpecCapability.advanced) && !f.required, + (f: OperandField) => hasDescriptor(f, SpecCapability.advanced) && !f.required, ), ); @@ -1041,11 +1041,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ )} {_.map(fieldList, (field) => ( - + ))} ))} @@ -1076,7 +1072,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ return ( {_.map(fieldList, (field) => ( - + ))} ); @@ -1084,7 +1080,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ const renderNormalFields = () => _.map(normalFields, (field) => ( - + )).filter(Boolean); const renderAdvancedFields = () => @@ -1095,7 +1091,7 @@ export const DEPRECATED_CreateOperandForm: React.FC = ({ textCollapsed={t('olm~Advanced configuration')} > {_.map(advancedFields, (field) => ( - + )).filter(Boolean)} @@ -1221,7 +1217,7 @@ type FlattenNestedPropertiesAccumulator = { type OperandFormInputGroupProps = { field: OperandField; - input: JSX.Element; + input: JSX.Element | null; }; type FieldGroupProps = { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx index 7ea9be06761..116db6f0650 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.tsx @@ -128,7 +128,7 @@ export const CreateOperand: React.FC = ({ { }; export const OperandStatus: React.FC = ({ operand }) => { - const { type, value }: OperandStatusType = getOperandStatus(operand); - if (!type || !value) { + const status = getOperandStatus(operand); + if (!status) { return <>-; } + const { type, value } = status; return ( @@ -284,9 +285,12 @@ export const OperandList: React.FC = (props) => { return obj; } const reference = props.kinds?.[0]; + if (!reference) { + return obj; // Return original object if no reference available + } return { - apiVersion: apiVersionForReference(reference as any), - kind: kindForReference(reference as any), + apiVersion: apiVersionForReference(reference), + kind: kindForReference(reference), ...obj, }; }) ?? [], @@ -298,7 +302,7 @@ export const OperandList: React.FC = (props) => { {...props} customSorts={{ operandStatus: getOperandStatusText, - getOperandNamespace: getOperandNamespace as (obj: any) => string, + getOperandNamespace: getOperandNamespace as (obj: K8sResourceKind) => string, }} data={data} EmptyMsg={() => @@ -445,7 +449,7 @@ export const ProvidedAPIsPage = (props: ProvidedAPIsPageProps) => { const [staticData, filteredData, onFilterChange] = useListPageFilter(data, rowFilters); const loaded = Object.values(resources).every((r) => r.loaded); // only pass the first loadError as StatusBox can only display one - const loadError: Record = Object.values(resources).find((r) => r.loadError) + const loadError: Record = Object.values(resources).find((r) => r.loadError) ?.loadError; return inFlight ? null : ( @@ -624,12 +628,7 @@ export const OperandDetails = connectToModel(({ crd, csv, kindObj, obj }: Operan crd?.spec?.versions?.find((v) => v.name === version)?.schema?.openAPIV3Schema ?? (definitionFor(kindObj) as JSONSchema7); - const { - podStatuses, - mainStatusDescriptor, - conditionsStatusDescriptors, - otherStatusDescriptors, - } = (statusDescriptors ?? []).reduce((acc, descriptor) => { + const statusDescriptorGroups = (statusDescriptors ?? []).reduce((acc, descriptor) => { if (isMainStatusDescriptor(descriptor)) { return { ...acc, @@ -660,6 +659,13 @@ export const OperandDetails = connectToModel(({ crd, csv, kindObj, obj }: Operan }; }, {} as any); + const { + podStatuses = [], + mainStatusDescriptor, + conditionsStatusDescriptors = [], + otherStatusDescriptors = [], + } = statusDescriptorGroups; + return (
@@ -782,7 +788,9 @@ const DefaultOperandDetailsPage = ({ k8sModel }: DefaultOperandDetailsPageProps) path: location.pathname.slice(0, location.pathname.lastIndexOf('/')), }, { - name: t('olm~{{item}} details', { item: kindForReference(params.plural as any) }), // Use url param in case model doesn't exist + name: t('olm~{{item}} details', { + item: params.plural ? kindForReference(params.plural) : '', + }), // Use url param in case model doesn't exist path: `${location.pathname}`, }, ]} @@ -803,8 +811,8 @@ const DefaultOperandDetailsPage = ({ k8sModel }: DefaultOperandDetailsPageProps) export const OperandDetailsPage = (props) => { const { plural, ns, name } = useParams(); - const resourceDetailsPage = useResourceDetailsPage(plural as any); - const [k8sModel, inFlight] = useK8sModel(plural); + const resourceDetailsPage = useResourceDetailsPage(plural || ''); + const [k8sModel, inFlight] = useK8sModel(plural || ''); if (inFlight && !k8sModel) { return null; } @@ -840,9 +848,9 @@ export type OperandListProps = { filters?: Filter[]; reduxID?: string; reduxIDs?: string[]; - rowSplitter?: any; - staticFilters?: any; - loadError?: Record; + rowSplitter?: unknown; + staticFilters?: Filter[]; + loadError?: Record; noAPIsFound?: boolean; showNamespace?: boolean; }; @@ -894,7 +902,7 @@ export type OperandDetailsProps = { crd: CustomResourceDefinitionKind; }; -type DefaultOperandDetailsPageProps = { customData: any; k8sModel: K8sModel }; +type DefaultOperandDetailsPageProps = { customData: unknown; k8sModel: K8sModel }; export type OperandResourceDetailsProps = { csv?: { data: ClusterServiceVersionKind }; @@ -907,7 +915,7 @@ type Header = { title: string; sortField?: string; sortFunc?: string; - transforms?: any; + transforms?: unknown; props: { className: string }; }; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx index c29ada4bbf5..7e07171f850 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/operand-form.tsx @@ -89,7 +89,7 @@ export const OperandForm: React.FC = ({ formContext={{ namespace: params.ns }} uiSchema={uiSchema} formData={formData} - onChange={onChange} + onChange={onChange as any} onError={setErrors} onSubmit={handleSubmit} onCancel={handleCancel} @@ -105,11 +105,11 @@ type ProvidedAPI = CRDDescription | APIServiceDefinition; export type OperandFormProps = { formData?: K8sResourceKind; - onChange?: (formData?: any) => void; + onChange?: (formData?: K8sResourceKind) => void; next?: string; csv: ClusterServiceVersionKind; model: K8sKind; providedAPI: ProvidedAPI; - prune?: (data: any) => any; + prune?: (data: K8sResourceKind) => K8sResourceKind; schema: JSONSchema7; }; diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/index.ts b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/index.ts index d72b718390e..0cb3e483ae1 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/index.ts +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/index.ts @@ -46,23 +46,28 @@ export type OperatorHubItem = { catalogSourceNamespace: string; categories: string[]; cloudCredentials: CloudCredentialKind; + capabilityLevel?: string | string[]; createdAt?: string; description: string; infraFeatures: InfrastructureFeature[]; infrastructure: InfrastructureKind; installed: boolean; installState?: InstalledState; + isInstalling: boolean; kind: string; longDescription: string; + marketplaceSupportWorkflow?: string; name: string; obj: PackageManifestKind; provider: string; + repository?: string; source?: string; subscription?: SubscriptionKind; tags: string[]; uid: string; validSubscription: string[]; - [key: string]: any; + version: string; + [key: string]: unknown; }; export enum OLMAnnotation { 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 831eca08078..940b35ac37b 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 @@ -406,7 +406,15 @@ export const OperatorHubItemDetails: React.FCC = ({ /> : notAvailable} + value={ + capability ? ( + + ) : ( + notAvailable + ) + } /> diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx index f7eb93896af..a86da157f9c 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-items.tsx @@ -207,8 +207,15 @@ export const getProviderValue = (value: string): string => { return value; }; -const sortFilterValues = (values: any[], field: string): any[] => { - let sorter: any = ['value']; +interface FilterValue { + label: string; + synonyms?: string[]; + value: string; + active: boolean; +} + +const sortFilterValues = (values: FilterValue[], field: string): FilterValue[] => { + let sorter: ((value: FilterValue) => string | number) | string[] = ['value']; if (field === 'provider') { sorter = ({ value }) => providerSort(value); @@ -237,23 +244,50 @@ const sortFilterValues = (values: any[], field: string): any[] => { return _.sortBy(values, sorter); }; +interface FilterState { + [key: string]: { + [value: string]: FilterValue; + }; +} + const determineAvailableFilters = ( - initialFilters: any, + initialFilters: FilterState, items: OperatorHubItem[], filterGroups: string[], -): any => { +): FilterState => { const filters = _.cloneDeep(initialFilters); _.each(filterGroups, (field) => { - const values: { label: any; synonyms?: any; value: any; active: boolean }[] = []; + const values: FilterValue[] = []; _.each(items, (item) => { - let value = item[field]; - let synonyms; + let value: string | string[] = ''; + let synonyms: string[] = []; + if (field === 'provider') { - value = getProviderValue(value); + value = getProviderValue(item.provider); synonyms = _.map(ignoredProviderTails, (tail) => `${value}${tail}`); + } else if (field === 'categories') { + value = item.categories?.map((cat) => cat.toLowerCase()) ?? []; + } else if (field === 'tags') { + value = item.tags?.map((tag) => tag.toLowerCase()) ?? []; + } else if (field === 'source') { + value = item.source ?? ''; + } else if (field === 'installState') { + value = item.installState ?? ''; + } else if (field === 'capabilityLevel') { + value = Array.isArray(item.capabilityLevel) + ? item.capabilityLevel + : item.capabilityLevel + ? [item.capabilityLevel] + : []; + } else if (field === 'infraFeatures') { + value = item.infraFeatures?.map((feature) => feature) ?? []; + } else if (field === 'validSubscriptionFilters') { + value = item.validSubscription?.map((sub) => sub) ?? []; + } else { + value = (item as any)[field] as string; } - if (value !== undefined && !Array.isArray(value)) { + if (value !== undefined && value !== '' && !Array.isArray(value)) { if (!_.some(values, { value })) { values.push({ label: value, @@ -266,7 +300,7 @@ const determineAvailableFilters = ( if (Array.isArray(value)) { _.each(value, (v) => { - if (!_.some(values, { v })) { + if (!_.some(values, { value: v })) { values.push({ label: v, synonyms, @@ -278,7 +312,7 @@ const determineAvailableFilters = ( } }); - _.forEach(sortFilterValues(values, field), (nextValue: any) => + _.forEach(sortFilterValues(values, field), (nextValue: FilterValue) => _.set(filters, [field, nextValue.value], nextValue), ); }); @@ -312,11 +346,11 @@ export const keywordCompare = (filterString: string, item: OperatorHubItem): boo if (!item) { return false; } - const keywords = item.keywords?.map((k) => k.toLowerCase()) ?? []; + const keywords = (item.keywords as string[])?.map((k) => k.toLowerCase()) ?? []; return ( item.name.toLowerCase().includes(filterString) || - _.get(item, 'obj.metadata.name', '').toLowerCase().includes(filterString) || + (_.get(item, 'obj.metadata.name', '') as string).toLowerCase().includes(filterString) || (item.description && item.description.toLowerCase().includes(filterString)) || keywords.includes(filterString) ); @@ -329,7 +363,7 @@ export const calculateRelevanceScore = (filterString: string, item: OperatorHubI } const searchTerm = filterString.toLowerCase(); - const keywords = item.keywords?.map((k) => k.toLowerCase()) ?? []; + const keywords = (item.keywords as string[])?.map((k) => k.toLowerCase()) ?? []; let score = 0; // Title/Name matches get highest weight @@ -415,7 +449,7 @@ const getRedHatPriority = (item: OperatorHubItem): number => { // Check metadata.labels.provider const metadataProvider = _.get(item, 'obj.metadata.labels.provider', ''); if (metadataProvider) { - const provider = metadataProvider.toLowerCase(); + const provider = (metadataProvider as string).toLowerCase(); if (/^red hat(,?\s?inc\.?)?$/.test(provider)) { return REDHAT_PRIORITY.EXACT_MATCH; // Highest priority for exact matches of 'red hat', 'red hat, inc.', 'red hat inc.', 'red hat inc' } @@ -513,7 +547,7 @@ export const orderAndSortByRelevance = ( ...itemWithoutScoring, // Preserve relevanceScore for console table debugging // eslint-disable-next-line no-underscore-dangle - relevanceScore: _scoringData.relevanceScore, + relevanceScore: (_scoringData as any).relevanceScore, }; }); }; @@ -524,9 +558,9 @@ const OperatorHubTile: React.FC = ({ item, onClick }) => { return null; } const { uid, name, imgUrl, provider, description, installed } = item; - const vendor = provider ? t('olm~provided by {{provider}}', { provider }) : null; + const vendor = provider ? (t('olm~provided by {{provider}}', { provider }) as string) : null; const badges = item?.source ? [] : []; - const icon = ; + const icon = ; const vendorAndDeprecated = () => ( <> {vendor} @@ -599,7 +633,7 @@ export const OperatorHubTileView: React.FC = (props) = }, [filteredItems]); const getAvailableFiltersFromAllItems = React.useCallback( - (initialFilters: any, _items: OperatorHubItem[], filterGroups: string[]) => { + (initialFilters: FilterState, _items: OperatorHubItem[], filterGroups: string[]) => { // Always use filteredItems (full list) instead of the passed items (which are already filtered) return determineAvailableFilters(initialFilters, filteredItems, filterGroups); }, @@ -641,7 +675,7 @@ export const OperatorHubTileView: React.FC = (props) = if (selectedCapabilityLevel !== 'all') { const capabilityLevelsToFilter = JSON.parse(selectedCapabilityLevel); items = items.filter((item) => { - const itemCapability = (item as any).capabilityLevel; + const itemCapability = item.capabilityLevel; if (itemCapability) { if (Array.isArray(itemCapability)) { return itemCapability.some((level) => capabilityLevelsToFilter.includes(level)); @@ -878,9 +912,12 @@ export const OperatorHubTileView: React.FC = (props) = if (selectedCategory !== 'all') activeFilters.push(`Category: ${selectedCategory}`); if (selectedSource !== 'all') activeFilters.push(`Source: ${selectedSource}`); if (selectedProvider !== 'all') activeFilters.push(`Provider: ${selectedProvider}`); - if (selectedCapabilityLevel !== 'all') activeFilters.push(`Capability Level: ${selectedCapabilityLevel}`); - if (selectedInfraFeatures !== 'all') activeFilters.push(`Infrastructure Features: ${selectedInfraFeatures}`); - if (selectedValidSubscriptionFilters !== 'all') activeFilters.push(`Valid Subscription: ${selectedValidSubscriptionFilters}`); + if (selectedCapabilityLevel !== 'all') + activeFilters.push(`Capability Level: ${selectedCapabilityLevel}`); + if (selectedInfraFeatures !== 'all') + activeFilters.push(`Infrastructure Features: ${selectedInfraFeatures}`); + if (selectedValidSubscriptionFilters !== 'all') + activeFilters.push(`Valid Subscription: ${selectedValidSubscriptionFilters}`); return activeFilters.length > 0 ? activeFilters.join(', ') : 'No filters applied'; }; @@ -896,9 +933,15 @@ export const OperatorHubTileView: React.FC = (props) = })) .filter((item) => item.relevanceScore > 0); const searchSortedForDisplay = orderAndSortByRelevance(searchFilteredForDisplay, searchTerm); - console.log('šŸ“‹ OperatorHub Items Array (Search Filtered & Sorted):', searchSortedForDisplay.map(item => item.name || 'N/A')); + console.log( + 'šŸ“‹ OperatorHub Items Array (Search Filtered & Sorted):', + searchSortedForDisplay.map((item) => item.name || 'N/A'), + ); } else { - console.log('šŸ“‹ OperatorHub Items Array:', sortedItems.map(item => item.name || 'N/A')); + console.log( + 'šŸ“‹ OperatorHub Items Array:', + sortedItems.map((item) => item.name || 'N/A'), + ); } // Debug: Log component state to identify race conditions @@ -907,7 +950,7 @@ export const OperatorHubTileView: React.FC = (props) = filteredItemsCount: filteredItems?.length || 0, sortedItemsCount: sortedItems?.length || 0, hasSearchTerm: !!searchTerm, - isInitialLoad: (!filteredItems || filteredItems.length === 0) + isInitialLoad: !filteredItems || filteredItems.length === 0, }); console.log(`šŸ“Œ Current Active Filters: ${getActiveFiltersDescription()}`); @@ -916,26 +959,30 @@ export const OperatorHubTileView: React.FC = (props) = // Use memoized sortedItems instead of recalculating if (searchTerm && sortedItems.length > 0) { - // For console display, filter items by search term since TileViewPage will do this later - const searchFilteredItems = sortedItems - .map((item) => ({ - ...item, - relevanceScore: calculateRelevanceScore(searchTerm, item), - })) - .filter((item) => item.relevanceScore > 0); - - // Sort the filtered items the same way TileViewPage will sort them - const searchSortedItems = orderAndSortByRelevance(searchFilteredItems, searchTerm); - - const tableData = searchSortedItems.map((item, index) => ({ + // For console display, filter items by search term since TileViewPage will do this later + const searchFilteredItems = sortedItems + .map((item) => ({ + ...item, + relevanceScore: calculateRelevanceScore(searchTerm, item), + })) + .filter((item) => item.relevanceScore > 0); + + // Sort the filtered items the same way TileViewPage will sort them + const searchSortedItems = orderAndSortByRelevance(searchFilteredItems, searchTerm); + + const tableData = searchSortedItems.map((item, index) => ({ Title: item.name || 'N/A', 'Search Relevance Score': item.relevanceScore || 0, - 'Is Red Hat Provider (Priority)': getRedHatPriority(item) === REDHAT_PRIORITY.EXACT_MATCH ? `Exact Match (${REDHAT_PRIORITY.EXACT_MATCH})` : - getRedHatPriority(item) === REDHAT_PRIORITY.CONTAINS_REDHAT ? `Contains Red Hat (${REDHAT_PRIORITY.CONTAINS_REDHAT})` : `Non-Red Hat (${REDHAT_PRIORITY.NON_REDHAT})`, + 'Is Red Hat Provider (Priority)': + getRedHatPriority(item) === REDHAT_PRIORITY.EXACT_MATCH + ? `Exact Match (${REDHAT_PRIORITY.EXACT_MATCH})` + : getRedHatPriority(item) === REDHAT_PRIORITY.CONTAINS_REDHAT + ? `Contains Red Hat (${REDHAT_PRIORITY.CONTAINS_REDHAT})` + : `Non-Red Hat (${REDHAT_PRIORITY.NON_REDHAT})`, // Source: item.source || 'N/A', 'Metadata Provider': _.get(item, 'obj.metadata.labels.provider', 'N/A'), 'Capability Level': (() => { - const itemCapability = (item as any).capabilityLevel; + const itemCapability = item.capabilityLevel; if (itemCapability) { if (Array.isArray(itemCapability)) { return itemCapability.join(', '); @@ -947,27 +994,39 @@ export const OperatorHubTileView: React.FC = (props) = const specCapability = _.get(item, 'obj.spec.capabilityLevel', ''); if (specCapability) return specCapability; - const metadataCapability = _.get(item, 'obj.metadata.annotations["operators.operatorframework.io/capability-level"]', ''); + const metadataCapability = _.get( + item, + 'obj.metadata.annotations["operators.operatorframework.io/capability-level"]', + '', + ); if (metadataCapability) return metadataCapability; return 'N/A'; })(), - 'Infrastructure Features': Array.isArray(item.infraFeatures) ? item.infraFeatures.join(', ') : 'N/A', + 'Infrastructure Features': Array.isArray(item.infraFeatures) + ? item.infraFeatures.join(', ') + : 'N/A', })); - console.log(`\nšŸ” OperatorHub Search Results for "${searchTerm}" (${searchSortedItems.length} matches)`); + console.log( + `\nšŸ” OperatorHub Search Results for "${searchTerm}" (${searchSortedItems.length} matches)`, + ); console.log(`šŸ“Œ Active Filters: ${getActiveFiltersDescription()}`); console.table(tableData); } else if (sortedItems.length > 0) { // Console table for filtered results without search term (category/filter-based) - using memoized data const tableData = sortedItems.map((item, index) => ({ Title: item.name || 'N/A', - 'Is Red Hat Provider (Priority)': getRedHatPriority(item) === REDHAT_PRIORITY.EXACT_MATCH ? `Exact Match (${REDHAT_PRIORITY.EXACT_MATCH})` : - getRedHatPriority(item) === REDHAT_PRIORITY.CONTAINS_REDHAT ? `Contains Red Hat (${REDHAT_PRIORITY.CONTAINS_REDHAT})` : `Non-Red Hat (${REDHAT_PRIORITY.NON_REDHAT})`, + 'Is Red Hat Provider (Priority)': + getRedHatPriority(item) === REDHAT_PRIORITY.EXACT_MATCH + ? `Exact Match (${REDHAT_PRIORITY.EXACT_MATCH})` + : getRedHatPriority(item) === REDHAT_PRIORITY.CONTAINS_REDHAT + ? `Contains Red Hat (${REDHAT_PRIORITY.CONTAINS_REDHAT})` + : `Non-Red Hat (${REDHAT_PRIORITY.NON_REDHAT})`, // Source: item.source || 'N/A', 'Metadata Provider': _.get(item, 'obj.metadata.labels.provider', 'N/A'), 'Capability Level': (() => { - const itemCapability = (item as any).capabilityLevel; + const itemCapability = item.capabilityLevel; if (itemCapability) { if (Array.isArray(itemCapability)) { return itemCapability.join(', '); @@ -979,12 +1038,18 @@ export const OperatorHubTileView: React.FC = (props) = const specCapability = _.get(item, 'obj.spec.capabilityLevel', ''); if (specCapability) return specCapability; - const metadataCapability = _.get(item, 'obj.metadata.annotations["operators.operatorframework.io/capability-level"]', ''); + const metadataCapability = _.get( + item, + 'obj.metadata.annotations["operators.operatorframework.io/capability-level"]', + '', + ); if (metadataCapability) return metadataCapability; return 'N/A'; })(), - 'Infrastructure Features': Array.isArray(item.infraFeatures) ? item.infraFeatures.join(', ') : 'N/A', + 'Infrastructure Features': Array.isArray(item.infraFeatures) + ? item.infraFeatures.join(', ') + : 'N/A', })); console.log(`\nšŸ“‚ OperatorHub Filtered Results (${tableData.length} items)`); @@ -1022,8 +1087,8 @@ export const OperatorHubTileView: React.FC = (props) = <> = (props) = 'co-catalog-page__overlay-action', )} data-test-id="operator-install-btn" - to={installLink as any} + to={installLink} > {t('olm~Install')} @@ -1054,7 +1119,7 @@ export const OperatorHubTileView: React.FC = (props) = className="co-catalog-page__overlay-action" data-test-id="operator-uninstall-btn" isDisabled={!detailsItem.installed} - onClick={() => history.push(uninstallLink() as any)} + onClick={() => history.push(uninstallLink())} variant="secondary" > {t('olm~Uninstall')} diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts index ab280ca2e68..0b317423515 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub-utils.spec.ts @@ -23,7 +23,7 @@ import { InfrastructureFeature, OLMAnnotation, ValidSubscriptionValue } from '.' describe('getPackageSource', () => { it('should handle undefined argument', () => { - const source = getPackageSource(undefined as any); + const source = getPackageSource(undefined); expect(source).toBeUndefined(); }); it('should return correct default Red Hat operator sources', () => { @@ -53,7 +53,7 @@ describe('getPackageSource', () => { describe('isAWSSTSCluster', () => { it('should handle undefined arguments', () => { - const result = isAWSSTSCluster(undefined as any, undefined as any, undefined as any); + const result = isAWSSTSCluster(undefined, undefined, undefined); expect(result).toEqual(false); }); it('should return true', () => { @@ -88,7 +88,7 @@ describe('isAWSSTSCluster', () => { describe('isAzureWIFCluster', () => { it('should handle undefined arguments', () => { - const result = isAzureWIFCluster(undefined as any, undefined as any, undefined as any); + const result = isAzureWIFCluster(undefined, undefined, undefined); expect(result).toEqual(false); }); it('should return true', () => { @@ -123,7 +123,7 @@ describe('isAzureWIFCluster', () => { describe('isGCPWIFCluster', () => { it('should handle undefined arguments', () => { - const result = isGCPWIFCluster(undefined as any, undefined as any, undefined as any); + const result = isGCPWIFCluster(undefined, undefined, undefined); expect(result).toEqual(false); }); it('should return true', () => { diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx index 7231bb47c32..8aed02938b1 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operator-hub/operator-hub.spec.tsx @@ -38,7 +38,7 @@ xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] OperatorHubList', () @@ -144,7 +144,13 @@ xdescribe(`[https://issues.redhat.com/browse/CONSOLE-2136] ${OperatorHubTileView }); it('updates filter counts on item changes', () => { - wrapper.setProps(operatorHubTileViewPagePropsWithDummy as any); + // Ensure the test data matches the expected OperatorHubItem[] type + // This prevents type errors due to missing required properties + wrapper.setProps({ + ...operatorHubTileViewPageProps, + ...operatorHubTileViewPagePropsWithDummy, + items: operatorHubTileViewPagePropsWithDummy.items as OperatorHubItem[], + }); wrapper.update(); const filterItemsChanged = wrapper.find(FilterSidePanelCategoryItem); @@ -213,7 +219,7 @@ describe(OperatorHubItemDetails.displayName || '', () => { expect(noMarkdown.exists()).toBe(false); - wrapper.setProps({ item: itemWithLongDescription as any }); + wrapper.setProps({ item: itemWithLongDescription }); wrapper.update(); const markdown = wrapper.find(MarkdownView); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx index 75ac1ce7897..b607b92e1af 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx @@ -41,7 +41,7 @@ describe('PackageManifestTableRow', () => { beforeEach(() => { jest.spyOn(UIActions, 'getActiveNamespace').mockReturnValue('default'); - const columns: any[] = []; + const columns: unknown[] = []; wrapper = shallow( { // This is to verify cataloSource column gets rendered on the Search page for PackageManifest resource it('renders column for catalog source for a package when no catalog source is defined', () => { const catalogSourceName = testPackageManifest.status.catalogSource; - const columns: any[] = []; + const columns: unknown[] = []; wrapper = shallow( , ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx index 8991f03f64d..b383eb8ce82 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx @@ -53,7 +53,8 @@ describe('SubscriptionTableRow', () => { const updateWrapper = () => { const rowArgs: RowFunctionArgs = { obj: subscription, - } as any; + columns: [], + }; wrapper = shallow(); return wrapper; diff --git a/frontend/packages/operator-lifecycle-manager/src/types.ts b/frontend/packages/operator-lifecycle-manager/src/types.ts index 64438568e1c..4231d565834 100644 --- a/frontend/packages/operator-lifecycle-manager/src/types.ts +++ b/frontend/packages/operator-lifecycle-manager/src/types.ts @@ -123,7 +123,7 @@ export type ClusterServiceVersionKind = { serviceAccountName: string; rules: { apiGroups: string[]; resources: string[]; verbs: string[] }[]; }[]; - deployments: { name: string; spec: any }[]; + deployments: { name: string; spec: unknown }[]; }; }; customresourcedefinitions?: { owned?: CRDDescription[]; required?: CRDDescription[] }; diff --git a/frontend/public/components/factory/modal.tsx b/frontend/public/components/factory/modal.tsx index c6537eb35da..946ae437790 100644 --- a/frontend/public/components/factory/modal.tsx +++ b/frontend/public/components/factory/modal.tsx @@ -20,7 +20,7 @@ export const createModal: CreateModal = (getModalElement) => { if (e && e.stopPropagation) { e.stopPropagation(); } - ReactDOM.unmountComponentAtNode(containerElement); + containerElement && ReactDOM.unmountComponentAtNode(containerElement); resolve(); }; Modal.setAppElement(document.getElementById('app-content')); @@ -134,7 +134,7 @@ export const ModalFooter: React.FC = ({ infoMessage={message} inProgress={inProgress} > - {children} + {children || null} ); }; @@ -157,12 +157,12 @@ export const ModalSubmitFooter: React.FC = ({ const { t } = useTranslation(); const onCancelClick = (e) => { e.stopPropagation(); - cancel(e); + cancel?.(e); }; const onResetClick = (e) => { e.stopPropagation(); - reset(e); + reset?.(e); }; const cancelButton = ( @@ -268,7 +268,7 @@ export type ModalSubmitFooterProps = { message?: string; errorMessage?: string; inProgress: boolean; - cancel: (e: React.SyntheticEvent) => void; + cancel?: (e: React.SyntheticEvent) => void; cancelText?: React.ReactNode; className?: string; resetText?: React.ReactNode;