From f5a75e452e700e233e373092421edd27a628e0fb Mon Sep 17 00:00:00 2001 From: cyril-ui-developer Date: Thu, 19 Feb 2026 15:36:59 -0500 Subject: [PATCH 1/3] Refactor console-shared-package from Firehose to useK8sWatchResource --- .../formik-fields/ResourceDropdownField.tsx | 36 ++-- .../buildconfig/sections/SecretsSection.tsx | 31 +++- .../buildconfig/sections/TriggersSection.tsx | 31 +++- .../__tests__/TriggersSection.spec.tsx | 158 ++++++++++-------- .../images/AdvancedImageOptions.tsx | 31 +++- .../image-search/ImageStreamDropdown.tsx | 56 +++++-- .../image-search/ImageStreamNsDropdown.tsx | 33 +++- .../pipeline/WebhookSection.tsx | 34 +++- .../CreateHelmChartRepositoryFormEditor.tsx | 74 ++++++-- .../add/event-sinks/KafkaSinkSection.tsx | 30 +++- .../__tests__/KafkaSinkSection.spec.tsx | 19 +-- .../form-fields/SourceResources.tsx | 76 ++++++++- .../form-fields/SinkResources.tsx | 128 ++++++++++++-- .../__tests__/SinkResources.spec.tsx | 43 +++-- .../dropdowns/ServiceAccountDropdown.tsx | 32 +++- .../pub-sub/form-fields/PubSubSubscriber.tsx | 80 ++++++++- .../src/components/sink-pubsub/SinkPubsub.tsx | 79 ++++++++- .../sink-pubsub/SinkPubsubModal.tsx | 4 +- .../sink-pubsub/__tests__/SinkPubsub.spec.tsx | 8 +- .../src/components/build-form/PVCDropdown.tsx | 27 ++- .../components/build-form/SecretDropdown.tsx | 31 +++- .../cloud-shell/setup/NamespaceSection.tsx | 36 +++- frontend/public/locales/en/public.json | 2 +- 23 files changed, 836 insertions(+), 243 deletions(-) diff --git a/frontend/packages/console-shared/src/components/formik-fields/ResourceDropdownField.tsx b/frontend/packages/console-shared/src/components/formik-fields/ResourceDropdownField.tsx index be29ad87631..9d1243ddc99 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/ResourceDropdownField.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/ResourceDropdownField.tsx @@ -2,8 +2,7 @@ import type { FC } from 'react'; import { FormGroup, FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core'; import type { FormikValues } from 'formik'; import { useField, useFormikContext } from 'formik'; -import { Firehose } from '@console/internal/components/utils/firehose'; -import type { FirehoseResource } from '@console/internal/components/utils/types'; +import type { FirehoseResult } from '@console/internal/components/utils/types'; import type { K8sResourceKind } from '@console/internal/module/k8s'; import { useFormikValidationFix } from '../../hooks/formik-validation-fix'; import type { ResourceDropdownItems } from '../dropdown/ResourceDropdown'; @@ -13,7 +12,7 @@ import { getFieldId } from './field-utils'; export interface ResourceDropdownFieldProps extends DropdownFieldProps { dataSelector: string[] | number[] | symbol[]; - resources: FirehoseResource[]; + resources: FirehoseResult[]; showBadge?: boolean; onLoad?: (items: ResourceDropdownItems) => void; onChange?: (key: string, name?: string | object, resource?: K8sResourceKind) => void; @@ -52,22 +51,21 @@ const ResourceDropdownField: FC = ({ return ( - - { - props.onChange && props.onChange(value, name, resource); - setFieldValue(props.name, value); - setFieldTouched(props.name, true); - }} - /> - + { + props.onChange && props.onChange(value, name, resource); + setFieldValue(props.name, value); + setFieldTouched(props.name, true); + }} + /> diff --git a/frontend/packages/dev-console/src/components/buildconfig/sections/SecretsSection.tsx b/frontend/packages/dev-console/src/components/buildconfig/sections/SecretsSection.tsx index 71a6b12519a..04a7c8d2836 100644 --- a/frontend/packages/dev-console/src/components/buildconfig/sections/SecretsSection.tsx +++ b/frontend/packages/dev-console/src/components/buildconfig/sections/SecretsSection.tsx @@ -1,9 +1,12 @@ import type { FC } from 'react'; +import { useMemo } from 'react'; import { TextInputTypes } from '@patternfly/react-core'; import * as fuzzy from 'fuzzysearch'; import { useTranslation } from 'react-i18next'; -import type { FirehoseResource } from '@console/internal/components/utils'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { SecretModel } from '@console/internal/models'; +import type { SecretKind } from '@console/internal/module/k8s'; +import { referenceForModel } from '@console/internal/module/k8s'; import { InputField, MultiColumnField, ResourceDropdownField } from '@console/shared'; import FormSection from '../../import/section/FormSection'; @@ -20,14 +23,30 @@ const SecretsSection: FC<{ namespace: string }> = ({ namespace }) => { const { t } = useTranslation(); const autocompleteFilter = (text: string, item: any): boolean => fuzzy(text, item?.props?.name); - const resources: FirehoseResource[] = [ - { + + const watchedResources = useK8sWatchResources<{ secrets: SecretKind[] }>({ + secrets: { isList: true, - kind: SecretModel.kind, - prop: SecretModel.id, + kind: referenceForModel(SecretModel), namespace, }, - ]; + }); + + const resources = useMemo( + () => [ + { + data: watchedResources.secrets.data, + loaded: watchedResources.secrets.loaded, + loadError: watchedResources.secrets.loadError, + kind: SecretModel.kind, + }, + ], + [ + watchedResources.secrets.data, + watchedResources.secrets.loaded, + watchedResources.secrets.loadError, + ], + ); const mountPointLabel = t('devconsole~Mount point'); diff --git a/frontend/packages/dev-console/src/components/buildconfig/sections/TriggersSection.tsx b/frontend/packages/dev-console/src/components/buildconfig/sections/TriggersSection.tsx index 07abbad2158..5d5e0e2c62c 100644 --- a/frontend/packages/dev-console/src/components/buildconfig/sections/TriggersSection.tsx +++ b/frontend/packages/dev-console/src/components/buildconfig/sections/TriggersSection.tsx @@ -1,10 +1,13 @@ import type { FC } from 'react'; +import { useMemo } from 'react'; import { FormGroup } from '@patternfly/react-core'; import { useField } from 'formik'; import * as fuzzy from 'fuzzysearch'; import { useTranslation } from 'react-i18next'; -import type { FirehoseResource } from '@console/internal/components/utils'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { SecretModel } from '@console/internal/models'; +import type { SecretKind } from '@console/internal/module/k8s'; +import { referenceForModel } from '@console/internal/module/k8s'; import { DropdownField, MultiColumnField, @@ -42,14 +45,30 @@ const TriggersSection: FC<{ namespace: string }> = ({ namespace }) => { }; const autocompleteFilter = (text: string, item: any): boolean => fuzzy(text, item?.props?.name); - const resources: FirehoseResource[] = [ - { + + const watchedResources = useK8sWatchResources<{ secrets: SecretKind[] }>({ + secrets: { isList: true, - kind: SecretModel.kind, - prop: SecretModel.id, + kind: referenceForModel(SecretModel), namespace, }, - ]; + }); + + const resources = useMemo( + () => [ + { + data: watchedResources.secrets.data, + loaded: watchedResources.secrets.loaded, + loadError: watchedResources.secrets.loadError, + kind: SecretModel.kind, + }, + ], + [ + watchedResources.secrets.data, + watchedResources.secrets.loaded, + watchedResources.secrets.loadError, + ], + ); return ( diff --git a/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/TriggersSection.spec.tsx b/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/TriggersSection.spec.tsx index 57a45758989..fe9b395d516 100644 --- a/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/TriggersSection.spec.tsx +++ b/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/TriggersSection.spec.tsx @@ -1,17 +1,38 @@ import type { FC, ReactNode } from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import type { FormikConfig } from 'formik'; import { Formik } from 'formik'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; +import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import type { TriggersSectionFormData } from '../TriggersSection'; import TriggersSection from '../TriggersSection'; -interface WrapperProps extends FormikConfig { - children?: ReactNode; +// Mock PatternFly topology to prevent console warnings during tests +jest.mock('@patternfly/react-topology', () => ({})); + +jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ + useK8sWatchResources: jest.fn(), +})); + +const mockUseK8sWatchResources = useK8sWatchResources as jest.Mock; + +const initialValues: TriggersSectionFormData = { + formData: { + triggers: { + configChange: false, + imageChange: false, + otherTriggers: [], + }, + }, +}; + +interface FormikWrapperProps { + children: ReactNode; + onSubmit: jest.Mock; } -const Wrapper: FC = ({ children, ...formikConfig }) => ( - +const FormikWrapper: FC = ({ children, onSubmit }) => ( + {(formikProps) => (
{children} @@ -21,100 +42,95 @@ const Wrapper: FC = ({ children, ...formikConfig }) => ( ); -const initialValues: TriggersSectionFormData = { - formData: { - triggers: { - configChange: false, - imageChange: false, - otherTriggers: [], - }, - }, +const renderTriggersSection = (onSubmit: jest.Mock) => { + renderWithProviders( + + + , + ); }; describe('TriggersSection', () => { - it('should render empty form', () => { + beforeEach(() => { + mockUseK8sWatchResources.mockReturnValue({ + secrets: { data: [], loaded: true, loadError: null }, + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should render all trigger configuration options', () => { const onSubmit = jest.fn(); - const renderResult = render( - - - , - ); + renderTriggersSection(onSubmit); - renderResult.getByTestId('section triggers'); - renderResult.getByText('Triggers'); - renderResult.getByText('Automatically build a new image when config changes'); - renderResult.getByText('Automatically build a new image when image changes'); - renderResult.getByText('Add trigger'); + expect(screen.getByTestId('section triggers')).toBeInTheDocument(); + expect(screen.getByText('Triggers')).toBeInTheDocument(); + expect( + screen.getByText('Automatically build a new image when config changes'), + ).toBeInTheDocument(); + expect( + screen.getByText('Automatically build a new image when image changes'), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add trigger' })).toBeInTheDocument(); - expect((renderResult.getByTestId('image-change checkbox') as HTMLInputElement).checked).toBe( - false, - ); + const imageChangeCheckbox = screen.getByTestId('image-change checkbox') as HTMLInputElement; + expect(imageChangeCheckbox).not.toBeChecked(); - expect(onSubmit).toHaveBeenCalledTimes(0); + expect(onSubmit).not.toHaveBeenCalled(); }); - it('should allow user to change config change checkbox trigger and save this data', async () => { + it('should toggle config change checkbox and submit form with updated value', async () => { const user = userEvent.setup(); const onSubmit = jest.fn(); + renderTriggersSection(onSubmit); - const renderResult = render( - - - , - ); - - // Change form - await user.click(renderResult.getByTestId('config-change checkbox')); + const configChangeCheckbox = screen.getByTestId('config-change checkbox'); + await user.click(configChangeCheckbox); - // Submit - const submitButton = renderResult.getByRole('button', { name: 'Submit' }); + const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); + await waitFor(() => { expect(onSubmit).toHaveBeenCalledTimes(1); }); - const expectedFormData: TriggersSectionFormData = { - formData: { - triggers: { - configChange: true, - imageChange: false, - otherTriggers: [], - }, - }, - }; - expect(onSubmit).toHaveBeenLastCalledWith(expectedFormData, expect.anything()); + expect(onSubmit.mock.calls[0][0]).toEqual( + expect.objectContaining({ + formData: expect.objectContaining({ + triggers: expect.objectContaining({ + configChange: true, + }), + }), + }), + ); }); - it('should allow user to change image change checkbox trigger and save this data', async () => { + it('should toggle image change checkbox and submit form with updated value', async () => { const user = userEvent.setup(); const onSubmit = jest.fn(); + renderTriggersSection(onSubmit); - const renderResult = render( - - - , - ); - - // Change form - await user.click(renderResult.getByTestId('image-change checkbox')); + const imageChangeCheckbox = screen.getByTestId('image-change checkbox'); + await user.click(imageChangeCheckbox); - // Submit - const submitButton = renderResult.getByRole('button', { name: 'Submit' }); + const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); + await waitFor(() => { expect(onSubmit).toHaveBeenCalledTimes(1); }); - const expectedFormData: TriggersSectionFormData = { - formData: { - triggers: { - configChange: false, - imageChange: true, - otherTriggers: [], - }, - }, - }; - expect(onSubmit).toHaveBeenLastCalledWith(expectedFormData, expect.anything()); + expect(onSubmit.mock.calls[0][0]).toEqual( + expect.objectContaining({ + formData: expect.objectContaining({ + triggers: expect.objectContaining({ + imageChange: true, + }), + }), + }), + ); }); }); diff --git a/frontend/packages/dev-console/src/components/deployments/images/AdvancedImageOptions.tsx b/frontend/packages/dev-console/src/components/deployments/images/AdvancedImageOptions.tsx index 6f87dbe5c91..0fba053a384 100644 --- a/frontend/packages/dev-console/src/components/deployments/images/AdvancedImageOptions.tsx +++ b/frontend/packages/dev-console/src/components/deployments/images/AdvancedImageOptions.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react'; +import { useMemo } from 'react'; import { Button, ButtonVariant } from '@patternfly/react-core'; import type { FormikValues } from 'formik'; import { useFormikContext } from 'formik'; @@ -6,7 +7,9 @@ import { useTranslation } from 'react-i18next'; import { useCreateSecretModal } from '@console/dev-console/src/components/import/CreateSecretModal'; import { SecretFormType } from '@console/internal/components/secrets/create-secret'; import { ExpandCollapse } from '@console/internal/components/utils'; +import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { SecretModel } from '@console/internal/models'; +import type { K8sResourceKind } from '@console/internal/module/k8s'; import { ResourceDropdownField } from '@console/shared/src'; const AdvancedImageOptions: FC = () => { @@ -28,6 +31,25 @@ const AdvancedImageOptions: FC = () => { const handleSave = (name: string) => { setFieldValue('formData.imagePullSecret', name); }; + + const [secretsData, secretsLoaded, secretsLoadError] = useK8sWatchResource({ + isList: true, + namespace, + kind: SecretModel.kind, + }); + + const resources = useMemo( + () => [ + { + data: secretsData, + loaded: secretsLoaded, + loadError: secretsLoadError, + kind: SecretModel.kind, + }, + ], + [secretsData, secretsLoaded, secretsLoadError], + ); + return ( { 'devconsole~Secret for authentication when pulling image from a secured registry.', )} placeholder={t('devconsole~Select Secret name')} - resources={[ - { - isList: true, - namespace, - kind: SecretModel.kind, - prop: 'secrets', - }, - ]} + resources={resources} resourceFilter={filterData} dataSelector={['metadata', 'name']} dataTest="secrets-dropdown" diff --git a/frontend/packages/dev-console/src/components/import/image-search/ImageStreamDropdown.tsx b/frontend/packages/dev-console/src/components/import/image-search/ImageStreamDropdown.tsx index 4c376233226..29eb552f730 100644 --- a/frontend/packages/dev-console/src/components/import/image-search/ImageStreamDropdown.tsx +++ b/frontend/packages/dev-console/src/components/import/image-search/ImageStreamDropdown.tsx @@ -1,12 +1,14 @@ import type { FC } from 'react'; -import { useContext, useCallback, useEffect } from 'react'; +import { useContext, useCallback, useEffect, useMemo, useRef } from 'react'; import type { FormikValues } from 'formik'; import { useFormikContext } from 'formik'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; +import { ImageStreamModel } from '@console/internal/models'; import type { K8sResourceKind } from '@console/internal/module/k8s'; +import { referenceForModel } from '@console/internal/module/k8s'; import { ResourceDropdownField } from '@console/shared'; -import { getImageStreamResource } from '../../../utils/imagestream-utils'; import { ImageStreamActions } from '../import-types'; import { ImageStreamContext } from './ImageStreamContext'; @@ -17,8 +19,7 @@ const ImageStreamDropdown: FC<{ className?: string; }> = ({ disabled = false, formContextField, reloadCount, className }) => { const { t } = useTranslation(); - // eslint-disable-next-line react-hooks/exhaustive-deps - const imgCollection = {}; + const imgCollection = useRef>>({}); const { values, setFieldValue, initialValues } = useFormikContext(); const { imageStream, formType } = _.get(values, formContextField) || values; @@ -29,10 +30,10 @@ const ImageStreamDropdown: FC<{ const isStreamsAvailable = isNamespaceSelected && hasImageStreams && !loading; const fieldPrefix = formContextField ? `${formContextField}.` : ''; const collectImageStreams = (namespace: string, resource: K8sResourceKind): void => { - if (!imgCollection[namespace]) { - imgCollection[namespace] = {}; + if (!imgCollection.current[namespace]) { + imgCollection.current[namespace] = {}; } - imgCollection[namespace][resource.metadata.name] = resource; + imgCollection.current[namespace][resource.metadata.name] = resource; }; const getTitle = () => { return loading && !isStreamsAvailable @@ -49,7 +50,7 @@ const ImageStreamDropdown: FC<{ img === imageStream.image ? imageStream.tag : '', ); formType !== 'edit' && setFieldValue(`${fieldPrefix}isi`, initialIsi); - const image = _.get(imgCollection, [imageStream.namespace, img], {}); + const image = _.get(imgCollection.current, [imageStream.namespace, img], {}); dispatch({ type: ImageStreamActions.setSelectedImageStream, value: image }); }, [ @@ -60,7 +61,6 @@ const ImageStreamDropdown: FC<{ imageStream.namespace, formType, initialIsi, - imgCollection, dispatch, ], ); @@ -84,6 +84,42 @@ const ImageStreamDropdown: FC<{ // eslint-disable-next-line react-hooks/exhaustive-deps }, [imageStream.image, isStreamsAvailable]); + const watchSpec = useMemo( + () => + imageStream.namespace + ? { + imagestreams: { + isList: true, + kind: referenceForModel(ImageStreamModel), + namespace: imageStream.namespace, + }, + } + : {}, + [imageStream.namespace], + ); + + const watchedResources = useK8sWatchResources<{ imagestreams?: K8sResourceKind[] }>(watchSpec); + + const resources = useMemo( + () => + imageStream.namespace + ? [ + { + data: watchedResources.imagestreams?.data, + loaded: watchedResources.imagestreams?.loaded, + loadError: watchedResources.imagestreams?.loadError, + kind: ImageStreamModel.kind, + }, + ] + : [], + [ + imageStream.namespace, + watchedResources.imagestreams?.data, + watchedResources.imagestreams?.loaded, + watchedResources.imagestreams?.loadError, + ], + ); + useEffect(() => { reloadCount && imageStream.image && onDropdownChange(imageStream.image); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -93,7 +129,7 @@ const ImageStreamDropdown: FC<{ ({ + projects: { + isList: true, + kind: referenceForModel(ProjectModel), + }, + }); + + const resources = useMemo( + () => [ + { + data: watchedResources.projects.data, + loaded: watchedResources.projects.loaded, + loadError: watchedResources.projects.loadError, + kind: ProjectModel.kind, + }, + ], + [ + watchedResources.projects.data, + watchedResources.projects.loaded, + watchedResources.projects.loadError, + ], + ); + useEffect(() => { imageStream.namespace && onDropdownChange(); }, [onDropdownChange, imageStream.namespace]); @@ -43,7 +70,7 @@ const ImageStreamNsDropdown: FC<{ title={imageStream.namespace || t('devconsole~Select Project')} fullWidth required - resources={getProjectResource()} + resources={resources} dataSelector={['metadata', 'name']} onChange={onDropdownChange} appendItems={{ openshift: BuilderImagesNamespace.Openshift }} diff --git a/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx b/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx index 65010f47fee..1195641780e 100644 --- a/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx +++ b/frontend/packages/dev-console/src/components/pipeline-section/pipeline/WebhookSection.tsx @@ -1,5 +1,5 @@ import type { FC, ReactNode, ReactElement } from 'react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Content, TextInputTypes, @@ -25,9 +25,10 @@ import { Trans, useTranslation } from 'react-i18next'; import { generateSecret } from '@console/dev-console/src/components/import/import-submit-utils'; import FormSection from '@console/dev-console/src/components/import/section/FormSection'; import { GitProvider } from '@console/git-service/src'; -import type { FirehoseResource } from '@console/internal/components/utils'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { SecretModel } from '@console/internal/models'; -import type { ConfigMapKind } from '@console/internal/module/k8s/types'; +import type { ConfigMapKind, SecretKind } from '@console/internal/module/k8s'; +import { referenceForModel } from '@console/internal/module/k8s'; import { RadioGroupField, InputField, @@ -73,15 +74,32 @@ const WebhookSection: FC = ({ pac, formContextField }) => setFieldValue(`${fieldPrefix}webhook.url`, ctlUrl); } }, [fieldPrefix, pac, setFieldValue]); + const autocompleteFilter = (text: string, item: any): boolean => fuzzy(text, item?.props?.name); - const resources: FirehoseResource[] = [ - { + + const watchedResources = useK8sWatchResources<{ secrets: SecretKind[] }>({ + secrets: { isList: true, - kind: SecretModel.kind, - prop: SecretModel.id, + kind: referenceForModel(SecretModel), namespace, }, - ]; + }); + + const resources = useMemo( + () => [ + { + data: watchedResources.secrets.data, + loaded: watchedResources.secrets.loaded, + loadError: watchedResources.secrets.loadError, + kind: SecretModel.kind, + }, + ], + [ + watchedResources.secrets.data, + watchedResources.secrets.loaded, + watchedResources.secrets.loadError, + ], + ); const generateWebhookSecret = () => { setWebhookSecret(generateSecret()); diff --git a/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/CreateHelmChartRepositoryFormEditor.tsx b/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/CreateHelmChartRepositoryFormEditor.tsx index bdd36a74669..698d94e3a3f 100644 --- a/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/CreateHelmChartRepositoryFormEditor.tsx +++ b/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/CreateHelmChartRepositoryFormEditor.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react'; +import { useMemo } from 'react'; import { TextInputTypes } from '@patternfly/react-core'; import type { FormikValues } from 'formik'; import { useFormikContext } from 'formik'; @@ -7,7 +8,9 @@ import { useTranslation } from 'react-i18next'; import FormSection from '@console/dev-console/src/components/import/section/FormSection'; import type { HelmChartRepositoryType } from '@console/helm-plugin/src/types/helm-types'; import { ExpandCollapse } from '@console/internal/components/utils'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { ConfigMapModel, SecretModel } from '@console/internal/models'; +import type { K8sResourceKind } from '@console/internal/module/k8s'; import { InputField, ResourceDropdownField, @@ -40,6 +43,57 @@ const CreateHelmChartRepositoryFormEditor: FC(); const autocompleteFilter = (strText, item): boolean => fuzzy(strText, item?.props?.name); + + const watchedResources = useK8sWatchResources<{ + configMaps: K8sResourceKind[]; + secrets: K8sResourceKind[]; + }>({ + configMaps: { + isList: true, + kind: ConfigMapModel.kind, + namespace: 'openshift-config', + optional: true, + }, + secrets: { + isList: true, + kind: SecretModel.kind, + namespace: 'openshift-config', + optional: true, + }, + }); + + const configMapResources = useMemo( + () => [ + { + data: watchedResources.configMaps?.data, + loaded: watchedResources.configMaps?.loaded, + loadError: watchedResources.configMaps?.loadError, + kind: ConfigMapModel.kind, + }, + ], + [ + watchedResources.configMaps?.data, + watchedResources.configMaps?.loaded, + watchedResources.configMaps?.loadError, + ], + ); + + const secretResources = useMemo( + () => [ + { + data: watchedResources.secrets?.data, + loaded: watchedResources.secrets?.loaded, + loadError: watchedResources.secrets?.loadError, + kind: SecretModel.kind, + }, + ], + [ + watchedResources.secrets?.data, + watchedResources.secrets?.loaded, + watchedResources.secrets?.loadError, + ], + ); + return ( {showScopeType && !existingRepo && ( @@ -110,15 +164,7 @@ const CreateHelmChartRepositoryFormEditor: FC = ({ title, namespace, fullWid const autocompleteFilter = (text: string, item: any): boolean => fuzzy(text, item?.props?.name); - const resources: FirehoseResource[] = [ - { + const watchedResources = useK8sWatchResources<{ secrets: SecretKind[] }>({ + secrets: { isList: true, - kind: SecretModel.kind, - prop: SecretModel.id, + kind: referenceForModel(SecretModel), namespace, }, - ]; + }); + + const resources = useMemo( + () => [ + { + data: watchedResources.secrets.data, + loaded: watchedResources.secrets.loaded, + loadError: watchedResources.secrets.loadError, + kind: SecretModel.kind, + }, + ], + [ + watchedResources.secrets.data, + watchedResources.secrets.loaded, + watchedResources.secrets.loadError, + ], + ); return ( diff --git a/frontend/packages/knative-plugin/src/components/add/event-sinks/__tests__/KafkaSinkSection.spec.tsx b/frontend/packages/knative-plugin/src/components/add/event-sinks/__tests__/KafkaSinkSection.spec.tsx index 60c4d6f209b..5a1ad450a1a 100644 --- a/frontend/packages/knative-plugin/src/components/add/event-sinks/__tests__/KafkaSinkSection.spec.tsx +++ b/frontend/packages/knative-plugin/src/components/add/event-sinks/__tests__/KafkaSinkSection.spec.tsx @@ -30,20 +30,18 @@ jest.mock('../../../../hooks', () => ({ describe('KafkaSinkSection', () => { const title = 'Kafka Sink'; - it('should render KafkaSink FormSection with proper title', () => { + beforeEach(() => { (useK8sWatchResources as jest.Mock).mockReturnValue({ - kafkas: { data: [], loaded: true }, - kafkaconnections: { data: [], loaded: true }, + secrets: { data: [], loaded: true, loadError: null }, }); + }); + + it('should render KafkaSink FormSection with proper title', () => { const { container } = render(); expect(container.querySelector('FormSection')).toBeInTheDocument(); }); it('should render BootstrapServers and Topic fields with required and secret as not required', () => { - (useK8sWatchResources as jest.Mock).mockReturnValue({ - kafkas: { data: [], loaded: true }, - kafkaconnections: { data: [], loaded: true }, - }); const { container } = render(); const bootstrapServersField = container.querySelector( '[data-test="kafkasink-bootstrapservers-field"]', @@ -57,10 +55,9 @@ describe('KafkaSinkSection', () => { expect(secretField).toBeInTheDocument(); }); - it('should render BootstrapServers and topic fields with even if kafkaconnections loaded failed', () => { - (useK8sWatchResources as jest.Mock).mockReturnValue({ - kafkas: { data: [], loaded: true }, - kafkaconnections: { data: null, loaded: false, loadError: 'Error' }, + it('should render BootstrapServers and topic fields even if secrets loaded failed', () => { + (useK8sWatchResources as jest.Mock).mockReturnValueOnce({ + secrets: { data: [], loaded: false, loadError: 'Error' }, }); const { container } = render(); const bootstrapServersField = container.querySelector( diff --git a/frontend/packages/knative-plugin/src/components/add/event-sinks/form-fields/SourceResources.tsx b/frontend/packages/knative-plugin/src/components/add/event-sinks/form-fields/SourceResources.tsx index 235906dc357..a92490ef488 100644 --- a/frontend/packages/knative-plugin/src/components/add/event-sinks/form-fields/SourceResources.tsx +++ b/frontend/packages/knative-plugin/src/components/add/event-sinks/form-fields/SourceResources.tsx @@ -1,5 +1,5 @@ import type { FC, ReactElement } from 'react'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { FormGroup, Alert, @@ -12,10 +12,12 @@ import { useFormikContext } from 'formik'; import * as fuzzy from 'fuzzysearch'; import { isEmpty } from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import type { K8sResourceKind } from '@console/internal/module/k8s'; +import { referenceForModel } from '@console/internal/module/k8s'; import { getFieldId, ResourceDropdownField } from '@console/shared'; -import { getDynamicChannelResourceList } from '../../../../utils/fetch-dynamic-eventsources-utils'; -import { knativeEventingResourcesBroker } from '../../../../utils/get-knative-resources'; +import { EventingBrokerModel } from '../../../../models'; +import { useChannelModels } from '../../../../utils/fetch-dynamic-eventsources-utils'; import { craftResourceKey } from '../../../pub-sub/pub-sub-utils'; export interface SourceResourcesProps { @@ -30,16 +32,74 @@ const SourceResources: FC = ({ namespace, isMoveSink }) => FormikValues >(); - const resourcesData = [ - ...getDynamicChannelResourceList(namespace), - ...knativeEventingResourcesBroker(namespace), - ]; + // Get dynamic channel models + const { loaded: channelsLoaded, eventSourceChannels: channels } = useChannelModels(); + + const watchSpec = useMemo(() => { + const spec: Record = { + brokers: { + isList: true, + kind: referenceForModel(EventingBrokerModel), + namespace, + optional: true, + }, + }; + + // Add dynamic channels when loaded + if (channelsLoaded && channels.length > 0) { + channels.forEach((model) => { + const ref = referenceForModel(model); + spec[ref] = { + isList: true, + kind: ref, + namespace, + optional: true, + }; + }); + } + + return spec; + }, [namespace, channelsLoaded, channels]); + + const watchedResources = useK8sWatchResources>(watchSpec); + + // Transform watched resources to expected format + const resourcesData = useMemo(() => { + const result = []; + + // Add broker resources + if (watchedResources.brokers) { + result.push({ + data: watchedResources.brokers.data, + loaded: watchedResources.brokers.loaded, + loadError: watchedResources.brokers.loadError, + kind: EventingBrokerModel.kind, + }); + } + + // Add dynamic channel resources + if (channelsLoaded && channels.length > 0) { + channels.forEach((model) => { + const ref = referenceForModel(model); + if (watchedResources[ref]) { + result.push({ + data: watchedResources[ref].data, + loaded: watchedResources[ref].loaded, + loadError: watchedResources[ref].loadError, + kind: model.kind, + }); + } + }); + } + + return result; + }, [watchedResources, channelsLoaded, channels]); const autocompleteFilter = (strText: string, item: ReactElement): boolean => fuzzy(strText, item?.props?.name); const fieldId = getFieldId('source-name', 'dropdown'); const onChange = useCallback( - (selectedValue, valueObj) => { + (_selectedValue, valueObj) => { const modelData = valueObj?.props?.model; const name = valueObj?.props?.name; if (name && modelData) { diff --git a/frontend/packages/knative-plugin/src/components/add/event-sources/form-fields/SinkResources.tsx b/frontend/packages/knative-plugin/src/components/add/event-sources/form-fields/SinkResources.tsx index 29fda5d4552..857113d05e1 100644 --- a/frontend/packages/knative-plugin/src/components/add/event-sources/form-fields/SinkResources.tsx +++ b/frontend/packages/knative-plugin/src/components/add/event-sources/form-fields/SinkResources.tsx @@ -1,5 +1,5 @@ import type { FC, ReactElement } from 'react'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { FormGroup, Alert, @@ -12,15 +12,16 @@ import { useFormikContext, useField } from 'formik'; import * as fuzzy from 'fuzzysearch'; import { isEmpty } from 'lodash'; import { useTranslation } from 'react-i18next'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import type { K8sResourceKind } from '@console/internal/module/k8s'; +import { referenceForModel } from '@console/internal/module/k8s'; import { getFieldId, ResourceDropdownField } from '@console/shared'; -import { getDynamicChannelResourceList } from '../../../../utils/fetch-dynamic-eventsources-utils'; import { - knativeServingResourcesServices, - knativeEventingResourcesBroker, - k8sServices, - knativeKafkaSinks, -} from '../../../../utils/get-knative-resources'; + ServiceModel as KnativeServiceModel, + EventingBrokerModel, + KafkaSinkModel, +} from '../../../../models'; +import { useChannelModels } from '../../../../utils/fetch-dynamic-eventsources-utils'; import { craftResourceKey } from '../../../pub-sub/pub-sub-utils'; import { SinkType } from '../../import-types'; @@ -62,13 +63,112 @@ const SinkResources: FC = ({ namespace, isMoveSink }) => { [setFieldValue, setFieldTouched, validateForm], ); const contextAvailable = isMoveSink ? false : !!initialValues.formData.sink.name; - const resourcesData = [ - ...k8sServices(namespace), - ...knativeServingResourcesServices(namespace), - ...getDynamicChannelResourceList(namespace), - ...knativeEventingResourcesBroker(namespace), - ...knativeKafkaSinks(namespace), - ]; + + // Get dynamic channel models + const { loaded: channelsLoaded, eventSourceChannels: channels } = useChannelModels(); + + // Build watch spec for static resources + const watchSpec = useMemo(() => { + const spec: Record = { + services: { + isList: true, + kind: 'Service', + namespace, + optional: true, + }, + ksservices: { + isList: true, + kind: referenceForModel(KnativeServiceModel), + namespace, + optional: true, + }, + brokers: { + isList: true, + kind: referenceForModel(EventingBrokerModel), + namespace, + optional: true, + }, + kafkasinks: { + isList: true, + kind: referenceForModel(KafkaSinkModel), + namespace, + optional: true, + }, + }; + + // Add dynamic channels when loaded + if (channelsLoaded && channels.length > 0) { + channels.forEach((model) => { + const ref = referenceForModel(model); + spec[ref] = { + isList: true, + kind: ref, + namespace, + optional: true, + }; + }); + } + + return spec; + }, [namespace, channelsLoaded, channels]); + + const watchedResources = useK8sWatchResources>(watchSpec); + + // Transform watched resources to expected format + const resourcesData = useMemo(() => { + const result = []; + + // Add static resources + if (watchedResources.services) { + result.push({ + data: watchedResources.services.data, + loaded: watchedResources.services.loaded, + loadError: watchedResources.services.loadError, + kind: 'Service', + }); + } + if (watchedResources.ksservices) { + result.push({ + data: watchedResources.ksservices.data, + loaded: watchedResources.ksservices.loaded, + loadError: watchedResources.ksservices.loadError, + kind: KnativeServiceModel.kind, + }); + } + if (watchedResources.brokers) { + result.push({ + data: watchedResources.brokers.data, + loaded: watchedResources.brokers.loaded, + loadError: watchedResources.brokers.loadError, + kind: EventingBrokerModel.kind, + }); + } + if (watchedResources.kafkasinks) { + result.push({ + data: watchedResources.kafkasinks.data, + loaded: watchedResources.kafkasinks.loaded, + loadError: watchedResources.kafkasinks.loadError, + kind: KafkaSinkModel.kind, + }); + } + + // Add dynamic channel resources + if (channelsLoaded && channels.length > 0) { + channels.forEach((model) => { + const ref = referenceForModel(model); + if (watchedResources[ref]) { + result.push({ + data: watchedResources[ref].data, + loaded: watchedResources[ref].loaded, + loadError: watchedResources[ref].loadError, + kind: model.kind, + }); + } + }); + } + + return result; + }, [watchedResources, channelsLoaded, channels]); const handleOnLoad = (resourceList: { [key: string]: string }) => { if (isEmpty(resourceList)) { diff --git a/frontend/packages/knative-plugin/src/components/add/event-sources/form-fields/__tests__/SinkResources.spec.tsx b/frontend/packages/knative-plugin/src/components/add/event-sources/form-fields/__tests__/SinkResources.spec.tsx index e113ab60a72..65e156e80a6 100644 --- a/frontend/packages/knative-plugin/src/components/add/event-sources/form-fields/__tests__/SinkResources.spec.tsx +++ b/frontend/packages/knative-plugin/src/components/add/event-sources/form-fields/__tests__/SinkResources.spec.tsx @@ -1,9 +1,18 @@ -import { render } from '@testing-library/react'; -import * as coFetchModule from '@console/dynamic-plugin-sdk/src/utils/fetch/console-fetch'; -import { mockChannelCRDData } from '../../../../../utils/__mocks__/dynamic-channels-crd-mock'; -import { fetchChannelsCrd } from '../../../../../utils/fetch-dynamic-eventsources-utils'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; +import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import SinkResources from '../SinkResources'; +// Mock PatternFly topology to prevent console warnings during tests +jest.mock('@patternfly/react-topology', () => ({})); + +jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ + useK8sWatchResources: jest.fn(), +})); + +jest.mock('../../../../../utils/fetch-dynamic-eventsources-utils', () => ({ + useChannelModels: jest.fn(() => ({ loaded: true, eventSourceChannels: [] })), +})); + jest.mock('@console/shared', () => ({ ResourceDropdownField: 'ResourceDropdownField', getFieldId: jest.fn(() => 'mocked-field-id'), @@ -15,23 +24,24 @@ jest.mock('formik', () => ({ initialValues: { formData: { sink: { name: 'test' } }, }, + setFieldValue: jest.fn(), + setFieldTouched: jest.fn(), + validateForm: jest.fn(), })), })); -jest.mock('@console/dynamic-plugin-sdk/src/utils/fetch/console-fetch', () => ({ - ...jest.requireActual('@console/dynamic-plugin-sdk/src/utils/fetch/console-fetch'), - consoleFetch: jest.fn(), +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), })); -const consoleFetchMock = coFetchModule.consoleFetch as jest.Mock; - describe('SinkResources', () => { beforeEach(() => { - consoleFetchMock.mockImplementation(() => - Promise.resolve({ - json: () => mockChannelCRDData, - }), - ); + (useK8sWatchResources as jest.Mock).mockReturnValue({ + services: { data: [], loaded: true, loadError: null }, + ksservices: { data: [], loaded: true, loadError: null }, + brokers: { data: [], loaded: true, loadError: null }, + kafkasinks: { data: [], loaded: true, loadError: null }, + }); }); afterEach(() => { @@ -39,14 +49,13 @@ describe('SinkResources', () => { }); it('should be able to sink to k8s service', () => { - const { container } = render(); + const { container } = renderWithProviders(); const resourceDropdownField = container.querySelector('ResourceDropdownField'); expect(resourceDropdownField).toBeInTheDocument(); }); it('should be able to sink to knative service, broker, k8s service and channels', async () => { - await fetchChannelsCrd(); - const { container } = render(); + const { container } = renderWithProviders(); const resourceDropdownField = container.querySelector('ResourceDropdownField'); expect(resourceDropdownField).toBeInTheDocument(); }); diff --git a/frontend/packages/knative-plugin/src/components/dropdowns/ServiceAccountDropdown.tsx b/frontend/packages/knative-plugin/src/components/dropdowns/ServiceAccountDropdown.tsx index 2acafbc33ce..c5be7a373df 100644 --- a/frontend/packages/knative-plugin/src/components/dropdowns/ServiceAccountDropdown.tsx +++ b/frontend/packages/knative-plugin/src/components/dropdowns/ServiceAccountDropdown.tsx @@ -1,8 +1,11 @@ import type { FC } from 'react'; +import { useMemo } from 'react'; import * as fuzzy from 'fuzzysearch'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; +import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { ServiceAccountModel } from '@console/internal/models'; +import type { K8sResourceKind } from '@console/internal/module/k8s'; import { getActiveNamespace } from '@console/internal/reducers/ui'; import type { RootState } from '@console/internal/redux'; import { ResourceDropdownField } from '@console/shared'; @@ -24,15 +27,26 @@ const ServiceAccountDropdown: FC = ({ }) => { const { t } = useTranslation(); const autocompleteFilter = (strText, item): boolean => fuzzy(strText, item?.props?.name); - const resources = [ - { - isList: true, - kind: ServiceAccountModel.kind, - namespace, - prop: ServiceAccountModel.id, - optional: true, - }, - ]; + + const [saData, saLoaded, saLoadError] = useK8sWatchResource({ + isList: true, + kind: ServiceAccountModel.kind, + namespace, + optional: true, + }); + + const resources = useMemo( + () => [ + { + data: saData, + loaded: saLoaded, + loadError: saLoadError, + kind: ServiceAccountModel.kind, + }, + ], + [saData, saLoaded, saLoadError], + ); + return ( = ({ autoSelect = true, cancel setResourceAlert(_.isEmpty(resourceList)); }; + const watchSpec = useMemo( + () => + namespace + ? { + services: { + isList: true, + kind: 'Service', + namespace, + optional: true, + }, + ksservices: { + isList: true, + kind: referenceForModel(KnativeServiceModel), + namespace, + optional: true, + }, + kafkasinks: { + isList: true, + kind: referenceForModel(KafkaSinkModel), + namespace, + optional: true, + }, + } + : {}, + [namespace], + ); + + const watchedResources = useK8sWatchResources<{ + services?: K8sResourceKind[]; + ksservices?: K8sResourceKind[]; + kafkasinks?: K8sResourceKind[]; + }>(watchSpec); + + const resources = useMemo( + () => + namespace + ? [ + { + data: watchedResources.services?.data, + loaded: watchedResources.services?.loaded, + loadError: watchedResources.services?.loadError, + kind: ServiceModel.kind, + }, + { + data: watchedResources.ksservices?.data, + loaded: watchedResources.ksservices?.loaded, + loadError: watchedResources.ksservices?.loadError, + kind: KnativeServiceModel.kind, + }, + { + data: watchedResources.kafkasinks?.data, + loaded: watchedResources.kafkasinks?.loaded, + loadError: watchedResources.kafkasinks?.loadError, + kind: KafkaSinkModel.kind, + }, + ] + : [], + [ + namespace, + watchedResources.services?.data, + watchedResources.services?.loaded, + watchedResources.services?.loadError, + watchedResources.ksservices?.data, + watchedResources.ksservices?.loaded, + watchedResources.ksservices?.loadError, + watchedResources.kafkasinks?.data, + watchedResources.kafkasinks?.loaded, + watchedResources.kafkasinks?.loadError, + ], + ); + // filter out resource which are owned by other resource const resourceFilter = ({ metadata }: K8sResourceKind) => !metadata?.ownerReferences?.length; @@ -85,7 +159,7 @@ const PubSubSubscriber: FC = ({ autoSelect = true, cancel )} = ({ source, resourceType, cancel, close } }, }; + const watchSpec = useMemo( + () => + namespace + ? { + services: { + isList: true, + kind: 'Service', + namespace, + optional: true, + }, + ksservices: { + isList: true, + kind: referenceForModel(KsServiceModel), + namespace, + optional: true, + }, + kafkasinks: { + isList: true, + kind: referenceForModel(KafkaSinkModel), + namespace, + optional: true, + }, + } + : {}, + [namespace], + ); + + const watchedResources = useK8sWatchResources<{ + services?: K8sResourceKind[]; + ksservices?: K8sResourceKind[]; + kafkasinks?: K8sResourceKind[]; + }>(watchSpec); + + const resourceDropdown = useMemo( + () => + namespace + ? [ + { + data: watchedResources.services?.data, + loaded: watchedResources.services?.loaded, + loadError: watchedResources.services?.loadError, + kind: 'Service', + }, + { + data: watchedResources.ksservices?.data, + loaded: watchedResources.ksservices?.loaded, + loadError: watchedResources.ksservices?.loadError, + kind: KsServiceModel.kind, + }, + { + data: watchedResources.kafkasinks?.data, + loaded: watchedResources.kafkasinks?.loaded, + loadError: watchedResources.kafkasinks?.loadError, + kind: KafkaSinkModel.kind, + }, + ] + : [], + [ + namespace, + watchedResources.services?.data, + watchedResources.services?.loaded, + watchedResources.services?.loadError, + watchedResources.ksservices?.data, + watchedResources.ksservices?.loaded, + watchedResources.ksservices?.loadError, + watchedResources.kafkasinks?.data, + watchedResources.kafkasinks?.loaded, + watchedResources.kafkasinks?.loadError, + ], + ); + const handleSubmit = (values: FormikValues, action: FormikHelpers) => { const updatePayload = sanitizeResourceName({ ...source, @@ -62,7 +135,7 @@ const SinkPubsub: FC = ({ source, resourceType, cancel, close } diff --git a/frontend/packages/knative-plugin/src/components/sink-pubsub/SinkPubsubModal.tsx b/frontend/packages/knative-plugin/src/components/sink-pubsub/SinkPubsubModal.tsx index c73e49ced82..3876dbf6d67 100644 --- a/frontend/packages/knative-plugin/src/components/sink-pubsub/SinkPubsubModal.tsx +++ b/frontend/packages/knative-plugin/src/components/sink-pubsub/SinkPubsubModal.tsx @@ -9,13 +9,13 @@ import { ModalBody, ModalSubmitFooter, } from '@console/internal/components/factory/modal'; -import type { FirehoseResource } from '@console/internal/components/utils'; +import type { FirehoseResult } from '@console/internal/components/utils/types'; import { ResourceDropdownField } from '@console/shared'; import { craftResourceKey } from '../pub-sub/pub-sub-utils'; export interface SinkPubsubModalProps { resourceName: string; - resourceDropdown: FirehoseResource[]; + resourceDropdown: FirehoseResult[]; labelTitle: string; cancel?: () => void; } diff --git a/frontend/packages/knative-plugin/src/components/sink-pubsub/__tests__/SinkPubsub.spec.tsx b/frontend/packages/knative-plugin/src/components/sink-pubsub/__tests__/SinkPubsub.spec.tsx index 1e683ffdede..24b78f4fba7 100644 --- a/frontend/packages/knative-plugin/src/components/sink-pubsub/__tests__/SinkPubsub.spec.tsx +++ b/frontend/packages/knative-plugin/src/components/sink-pubsub/__tests__/SinkPubsub.spec.tsx @@ -28,8 +28,12 @@ jest.mock('@console/internal/module/k8s', () => ({ }, })); -jest.mock('../../../utils/get-knative-resources', () => ({ - getSinkableResources: jest.fn(), +jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ + useK8sWatchResources: jest.fn(() => ({ + services: { data: [], loaded: true, loadError: undefined }, + ksservices: { data: [], loaded: true, loadError: undefined }, + kafkasinks: { data: [], loaded: true, loadError: undefined }, + })), })); jest.mock('../../pub-sub/pub-sub-utils', () => ({ diff --git a/frontend/packages/shipwright-plugin/src/components/build-form/PVCDropdown.tsx b/frontend/packages/shipwright-plugin/src/components/build-form/PVCDropdown.tsx index 8cab5e40f6b..be7c6d73fbb 100644 --- a/frontend/packages/shipwright-plugin/src/components/build-form/PVCDropdown.tsx +++ b/frontend/packages/shipwright-plugin/src/components/build-form/PVCDropdown.tsx @@ -1,7 +1,11 @@ import type { FC } from 'react'; +import { useMemo } from 'react'; import * as fuzzy from 'fuzzysearch'; import { useTranslation } from 'react-i18next'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { PersistentVolumeClaimModel } from '@console/internal/models'; +import type { PersistentVolumeClaimKind } from '@console/internal/module/k8s'; +import { referenceForModel } from '@console/internal/module/k8s'; import { ResourceDropdownField } from '@console/shared'; interface PVCDropdownProps { @@ -12,15 +16,28 @@ interface PVCDropdownProps { const PVCDropdown: FC = ({ name, namespace }) => { const { t } = useTranslation(); const autocompleteFilter = (strText, item): boolean => fuzzy(strText, item?.props?.name); - const resources = [ - { + + const watchedResources = useK8sWatchResources<{ pvcs: PersistentVolumeClaimKind[] }>({ + pvcs: { isList: true, - kind: PersistentVolumeClaimModel.kind, + kind: referenceForModel(PersistentVolumeClaimModel), namespace, - prop: PersistentVolumeClaimModel.id, optional: true, }, - ]; + }); + + const resources = useMemo( + () => [ + { + data: watchedResources.pvcs.data, + loaded: watchedResources.pvcs.loaded, + loadError: watchedResources.pvcs.loadError, + kind: PersistentVolumeClaimModel.kind, + }, + ], + [watchedResources.pvcs.data, watchedResources.pvcs.loaded, watchedResources.pvcs.loadError], + ); + return ( = ({ name, namespace }) => { const { t } = useTranslation(); const autocompleteFilter = (strText, item): boolean => fuzzy(strText, item?.props?.name); - const resources = [ - { + + const watchedResources = useK8sWatchResources<{ secrets: SecretKind[] }>({ + secrets: { isList: true, - kind: SecretModel.kind, + kind: referenceForModel(SecretModel), namespace, - prop: SecretModel.id, optional: true, }, - ]; + }); + + const resources = useMemo( + () => [ + { + data: watchedResources.secrets.data, + loaded: watchedResources.secrets.loaded, + loadError: watchedResources.secrets.loadError, + kind: SecretModel.kind, + }, + ], + [ + watchedResources.secrets.data, + watchedResources.secrets.loaded, + watchedResources.secrets.loadError, + ], + ); + return ( = ({ flags }) => { [setFieldValue, setFieldTouched], ); + const watchedResources = useK8sWatchResources<{ projects: K8sResourceKind[] }>({ + projects: { + isList: true, + kind: referenceForModel(ProjectModel), + }, + }); + + const resources = useMemo( + () => [ + { + data: watchedResources.projects.data, + loaded: watchedResources.projects.loaded, + loadError: watchedResources.projects.loadError, + kind: ProjectModel.kind, + }, + ], + [ + watchedResources.projects.data, + watchedResources.projects.loaded, + watchedResources.projects.loadError, + ], + ); + const handleOnLoad = (projectList: { [key: string]: string }) => { const noProjects = _.isEmpty(projectList); if (noProjects || !projectList[namespace.value]) { @@ -54,13 +80,7 @@ const NamespaceSection: FC = ({ flags }) => { fullWidth required selectedKey={namespace.value} - resources={[ - { - isList: true, - kind: ProjectModel.kind, - prop: ProjectModel.id, - }, - ]} + resources={resources} dataSelector={['metadata', 'name']} onChange={onDropdownChange} actionItems={ diff --git a/frontend/public/locales/en/public.json b/frontend/public/locales/en/public.json index 0eb6b11ccba..101bb3fc26c 100644 --- a/frontend/public/locales/en/public.json +++ b/frontend/public/locales/en/public.json @@ -1799,11 +1799,11 @@ "unknown host": "unknown host", "Unknown user": "Unknown user", "Just now": "Just now", + "Select service account": "Select service account", "Delete {{kind}}": "Delete {{kind}}", "Disable": "Disable", "Disabled": "Disabled", "Enabled": "Enabled", - "Select service account": "Select service account", "Failed to load kubecontrollermanager": "Failed to load kubecontrollermanager", "Failed to parse cloud provider config {{cm}}": "Failed to parse cloud provider config {{cm}}", "The following content was expected to be defined in the configMap: {{ expectedValues }}": "The following content was expected to be defined in the configMap: {{ expectedValues }}", From 722d5551439fec25ed90e7e1051a843910b6ecad Mon Sep 17 00:00:00 2001 From: cyril-ui-developer Date: Wed, 7 Jan 2026 15:18:57 -0500 Subject: [PATCH 2/3] Refactor public/ Directory Components to useK8sWatchResource(s) --- .../src/actions/hooks/useBindingActions.ts | 8 +- .../__tests__/HelmReleaseResources.spec.tsx | 20 ++- .../locales/en/olm-v1.json | 1 + .../ServiceAccountDropdown.tsx | 2 +- frontend/public/components/RBAC/bindings.tsx | 130 ++++++++++------ .../components/__tests__/container.spec.tsx | 17 ++- .../cluster-settings/cluster-settings.tsx | 56 ++++--- .../public/components/command-line-tools.tsx | 60 +++++--- .../public/components/console-notifier.tsx | 66 ++++---- frontend/public/components/container.tsx | 34 ++--- frontend/public/components/create-yaml.tsx | 36 +++-- frontend/public/components/cron-job.tsx | 143 ++++++++++-------- frontend/public/components/edit-yaml.tsx | 52 +++---- .../factory/__tests__/details.spec.tsx | 57 +++++-- .../factory/__tests__/list-page.spec.tsx | 27 ++-- .../public/components/factory/details.tsx | 118 +++++++++------ .../public/components/factory/list-page.tsx | 126 ++++++++++----- .../components/instantiate-template.tsx | 39 +++-- .../alertmanager/alertmanager-config.tsx | 23 ++- .../alertmanager/alertmanager-yaml-editor.tsx | 23 ++- .../alert-manager-receiver-forms.tsx | 41 ++--- frontend/public/components/namespace-bar.tsx | 42 +++-- .../public/components/storage-class-form.tsx | 33 ++-- .../components/utils/horizontal-nav.tsx | 7 +- .../public/components/utils/list-dropdown.tsx | 47 +++++- .../utils/storage-class-dropdown.tsx | 24 ++- frontend/public/locales/en/public.json | 1 + 27 files changed, 757 insertions(+), 476 deletions(-) diff --git a/frontend/packages/console-app/src/actions/hooks/useBindingActions.ts b/frontend/packages/console-app/src/actions/hooks/useBindingActions.ts index 33265b41329..2173086d6e0 100644 --- a/frontend/packages/console-app/src/actions/hooks/useBindingActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/useBindingActions.ts @@ -42,11 +42,11 @@ export const useBindingActions = ( const navigate = useNavigate(); const [commonActions] = useCommonActions(model, obj, [CommonActionCreator.Delete] as const); - const { subjectIndex, subjects = [] } = obj; + const { subjectIndex, subjects } = obj ?? {}; const subject = subjects?.[subjectIndex]; const deleteBindingSubject = useWarningModal({ title: t('public~Delete {{label}} subject?', { - label: model.kind, + label: model?.kind, }), children: t('public~Are you sure you want to delete subject {{name}} of type {{kind}}?', { name: subject?.name, @@ -143,9 +143,9 @@ export const useBindingActions = ( : []), factory.DuplicateBinding(), factory.EditBindingSubject(), - ...(subjects.length === 1 ? [commonActions.Delete] : [factory.DeleteBindingSubject()]), + ...(subjects?.length === 1 ? [commonActions.Delete] : [factory.DeleteBindingSubject()]), ]; - }, [memoizedFilterActions, subject?.kind, factory, subjects.length, commonActions.Delete]); + }, [memoizedFilterActions, subject?.kind, factory, subjects?.length, commonActions.Delete]); return actions; }; diff --git a/frontend/packages/helm-plugin/src/components/details-page/resources/__tests__/HelmReleaseResources.spec.tsx b/frontend/packages/helm-plugin/src/components/details-page/resources/__tests__/HelmReleaseResources.spec.tsx index dce6f4187f4..f142ccb8eca 100644 --- a/frontend/packages/helm-plugin/src/components/details-page/resources/__tests__/HelmReleaseResources.spec.tsx +++ b/frontend/packages/helm-plugin/src/components/details-page/resources/__tests__/HelmReleaseResources.spec.tsx @@ -1,6 +1,7 @@ import type { ComponentProps } from 'react'; import { screen } from '@testing-library/react'; import * as ReactRouter from 'react-router-dom-v5-compat'; +import * as k8sWatchHook from '@console/internal/components/utils/k8s-watch-hook'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import { mockHelmReleases } from '../../../__tests__/helm-release-mock-data'; import HelmReleaseResources from '../HelmReleaseResources'; @@ -10,15 +11,28 @@ jest.mock('react-router-dom-v5-compat', () => ({ useParams: jest.fn(), })); +jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ + useK8sWatchResources: jest.fn(() => ({})), + useK8sWatchResource: jest.fn(() => [null, true, null]), +})); + +const mockUseK8sWatchResources = k8sWatchHook.useK8sWatchResources as jest.Mock; + describe('HelmReleaseResources', () => { const helmReleaseResourcesProps: ComponentProps = { customData: mockHelmReleases[0], }; - it('should render the MultiListPage component', () => { + it('should render the MultiListPage component and display empty state when no resources exist', () => { jest.spyOn(ReactRouter, 'useParams').mockReturnValue({ ns: 'test-helm' }); + + // mockHelmReleases[0] has an empty manifest, so no resources to watch renderWithProviders(); - // MultiListPage typically renders a list/table of resources - expect(screen.getByText('No Resources found')).toBeTruthy(); + + // Verify useK8sWatchResources hook was called (confirms migration from Firehose to hooks) + expect(mockUseK8sWatchResources).toHaveBeenCalled(); + + // Verify empty state message is displayed (user-visible content) + expect(screen.getByText('No Resources found')).toBeVisible(); }); }); diff --git a/frontend/packages/operator-lifecycle-manager-v1/locales/en/olm-v1.json b/frontend/packages/operator-lifecycle-manager-v1/locales/en/olm-v1.json index d61b0f1ff4d..76a1e8430bc 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/locales/en/olm-v1.json +++ b/frontend/packages/operator-lifecycle-manager-v1/locales/en/olm-v1.json @@ -48,6 +48,7 @@ "An error occurred. Please try again.": "An error occurred. Please try again.", "Create ServiceAccount": "Create ServiceAccount", "An error occurred": "An error occurred", + "Select service account": "Select service account", "Operator Lifecycle Management version 1": "Operator Lifecycle Management version 1", "Learn more about OLMv1": "Learn more about OLMv1", "With OLMv1, you'll get a much simpler API that's easier to work with and understand. Plus, you have more direct control over updates. You can define update ranges and decide exactly how they are rolled out.": "With OLMv1, you'll get a much simpler API that's easier to work with and understand. Plus, you have more direct control over updates. You can define update ranges and decide exactly how they are rolled out.", diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/components/cluster-extension/ServiceAccountDropdown.tsx b/frontend/packages/operator-lifecycle-manager-v1/src/components/cluster-extension/ServiceAccountDropdown.tsx index 45644d0022c..ea5dad6d61e 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/components/cluster-extension/ServiceAccountDropdown.tsx +++ b/frontend/packages/operator-lifecycle-manager-v1/src/components/cluster-extension/ServiceAccountDropdown.tsx @@ -66,7 +66,7 @@ export const ServiceAccountDropdown: FC = ({ : [] } desc={ServiceAccountModel.label} - placeholder={t('public~Select service account')} + placeholder={t('olm-v1~Select service account')} selectedKey={selectedKey} selectedKeyKind={ServiceAccountModel.kind} onChange={handleOnChange} diff --git a/frontend/public/components/RBAC/bindings.tsx b/frontend/public/components/RBAC/bindings.tsx index 2083c656991..b4ccbf5a754 100644 --- a/frontend/public/components/RBAC/bindings.tsx +++ b/frontend/public/components/RBAC/bindings.tsx @@ -47,7 +47,6 @@ import { TableColumn } from '@console/internal/module/k8s'; import { GetDataViewRows, ResourceFilters } from '@console/app/src/components/data-view/types'; import { tableFilters } from '../factory/table-filters'; import { ButtonBar } from '../utils/button-bar'; -import { Firehose } from '../utils/firehose'; import { getQueryArgument } from '../utils/router'; import { kindObj } from '../utils/inject'; import type { ListDropdownProps } from '../utils/list-dropdown'; @@ -57,7 +56,7 @@ import { ResourceName } from '../utils/resource-icon'; import { StatusBox, LoadingBox } from '../utils/status-box'; import { useAccessReview } from '../utils/rbac'; import { flagPending } from '../../reducers/features'; -import { useK8sWatchResources } from '../utils/k8s-watch-hook'; +import { useK8sWatchResource, useK8sWatchResources } from '../utils/k8s-watch-hook'; // Split each binding into one row per subject export const flatten = (resources): BindingKind[] => @@ -185,18 +184,19 @@ const bindingType = (binding: BindingKind) => { if (!binding) { return undefined; } - if (binding.roleRef.name.startsWith('system:')) { + if (binding.roleRef?.name?.startsWith('system:')) { return 'system'; } - return binding.metadata.namespace ? 'namespace' : 'cluster'; + return binding.metadata?.namespace ? 'namespace' : 'cluster'; }; const getDataViewRows: GetDataViewRows = (data, columns) => { - return data.map(({ obj: binding }) => { + return data.map((row) => { + const binding = row.obj; const rowCells = { [tableColumnInfo[0].id]: { cell: , - props: getNameCellProps(binding.metadata.name), + props: getNameCellProps(binding.metadata?.name), }, [tableColumnInfo[1].id]: { cell: , @@ -208,7 +208,7 @@ const getDataViewRows: GetDataViewRows = (data, columns) => { cell: binding.subject.name, }, [tableColumnInfo[4].id]: { - cell: binding.metadata.namespace ? ( + cell: binding.metadata?.namespace ? ( ) : ( i18next.t('public~All namespaces') @@ -360,11 +360,24 @@ export const RoleBindingsPage: FC = ({ }, }); - const data = useMemo(() => flatten(resources), [resources]); + // Only flatten when at least one resource has data to prevent undefined iteration + const data = useMemo(() => { + const hasData = Object.values(resources).some((r) => r.data); + return hasData ? flatten(resources) : []; + }, [resources]); + + const loaded = useMemo( + () => + Object.values(resources) + .filter((r) => !r.loadError) + .every((r) => r.loaded), + [resources], + ); - const loaded = Object.values(resources) - .filter((r) => !r.loadError) - .every((r) => r.loaded); + // Aggregate errors from all resources + const loadError = useMemo(() => Object.values(resources).find((r) => r.loadError)?.loadError, [ + resources, + ]); return ( <> @@ -377,7 +390,7 @@ export const RoleBindingsPage: FC = ({ @@ -784,52 +797,79 @@ const getSubjectIndex = () => { }; const BindingLoadingWrapper: FC = (props) => { + const { obj, loaded, loadError, fixedKeys } = props; const [, setActiveNamespace] = useActiveNamespace(); + + if (!loaded) { + return ; + } + + if (loadError) { + return ; + } + + if (!obj || _.isEmpty(obj)) { + return ; + } + const fixed: { [key: string]: any } = {}; - _.each(props.fixedKeys, (k) => (fixed[k] = _.get(props.obj.data, k))); + fixedKeys.forEach((k) => (fixed[k] = obj?.[k])); + return ( - - - + ); }; export const EditRoleBinding: FC = ({ kind }) => { const { t } = useTranslation(); const params = useParams(); + + const [obj, loaded, loadError] = useK8sWatchResource({ + kind, + name: params.name, + namespace: params.ns, + isList: false, + }); + return ( - - - + ); }; export const CopyRoleBinding: FC = ({ kind }) => { const { t } = useTranslation(); const params = useParams(); + + const [obj, loaded, loadError] = useK8sWatchResource({ + kind, + name: params.name, + namespace: params.ns, + isList: false, + }); + return ( - - - + ); }; @@ -844,7 +884,7 @@ type BindingProps = { type BindingsListTableProps = { data: BindingKind[]; loaded: boolean; - loadError: string; + loadError?: unknown; mock?: boolean; staticFilters?: any; }; @@ -881,9 +921,9 @@ type BindingLoadingWrapperProps = { titleVerbAndKind: string; saveButtonText?: string; isCreate?: boolean; - obj?: { - data: RoleBindingKind | ClusterRoleBindingKind; - }; + obj?: RoleBindingKind | ClusterRoleBindingKind; + loaded: boolean; + loadError?: unknown; }; type EditRoleBindingProps = { diff --git a/frontend/public/components/__tests__/container.spec.tsx b/frontend/public/components/__tests__/container.spec.tsx index a9c44ff8bb4..9553ffbafdd 100644 --- a/frontend/public/components/__tests__/container.spec.tsx +++ b/frontend/public/components/__tests__/container.spec.tsx @@ -29,8 +29,8 @@ jest.mock('../utils/scroll-to-top-on-mount', () => ({ ScrollToTopOnMount: ({ children }: { children }) => children || null, })); -jest.mock('../utils/firehose', () => ({ - Firehose: jest.fn(({ children }) => children), +jest.mock('../utils/k8s-watch-hook', () => ({ + useK8sWatchResource: jest.fn(() => [null, false, null]), })); const mockUseParams = ReactRouter.useParams as jest.Mock; @@ -38,6 +38,7 @@ const mockUseLocation = ReactRouter.useLocation as jest.Mock; const mockReactRouterUseLocation = useLocation as jest.Mock; const mockUseFavoritesOptions = require('@console/internal/components/useFavoritesOptions') .useFavoritesOptions as jest.Mock; +const mockUseK8sWatchResource = require('../utils/k8s-watch-hook').useK8sWatchResource as jest.Mock; describe('ContainersDetailsPage', () => { beforeEach(() => { @@ -51,6 +52,7 @@ describe('ContainersDetailsPage', () => { }); it('verifies loading state while container data is being fetched', () => { + mockUseK8sWatchResource.mockReturnValue([null, false, null]); renderWithProviders(); expect(screen.getByRole('progressbar', { name: 'Contents' })).toBeVisible(); @@ -58,7 +60,7 @@ describe('ContainersDetailsPage', () => { }); describe('ContainerDetails', () => { - const obj = { data: { ...testPodInstance } }; + const pod = { ...testPodInstance }; beforeEach(() => { jest.clearAllMocks(); @@ -79,11 +81,12 @@ describe('ContainerDetails', () => { }); await act(async () => { - renderWithProviders(); + renderWithProviders(); }); expect(screen.getByText('crash-app')).toBeVisible(); - expect(screen.getByText('Waiting')).toBeVisible(); + // Verify "Waiting" appears in both the page heading and details section + expect(screen.getAllByText('Waiting')).toHaveLength(2); }); it('verifies the 404 error page when user tries to access non-existent container', async () => { @@ -94,7 +97,7 @@ describe('ContainerDetails', () => { }); await act(async () => { - renderWithProviders(); + renderWithProviders(); }); expect(screen.getByRole('heading', { name: '404: Page Not Found' })).toBeVisible(); @@ -110,7 +113,7 @@ describe('ContainerDetails', () => { }); await act(async () => { - renderWithProviders(); + renderWithProviders(); }); expect(screen.getByRole('progressbar', { name: 'Contents' })).toBeVisible(); diff --git a/frontend/public/components/cluster-settings/cluster-settings.tsx b/frontend/public/components/cluster-settings/cluster-settings.tsx index abb4d6fe163..205935e6969 100644 --- a/frontend/public/components/cluster-settings/cluster-settings.tsx +++ b/frontend/public/components/cluster-settings/cluster-settings.tsx @@ -88,8 +88,6 @@ import { ExternalLink } from '@console/shared/src/components/links/ExternalLink' import { documentationURLs, getDocumentationURL, isManaged } from '../utils/documentation'; import { EmptyBox } from '../utils/status-box'; import { FieldLevelHelp } from '../utils/field-level-help'; -import { Firehose } from '../utils/firehose'; -import type { FirehoseResource } from '../utils/types'; import { HorizontalNav } from '../utils/horizontal-nav'; import { ReleaseNotesLink } from '../utils/release-notes-link'; import { ResourceLink, resourcePathFromModel } from '../utils/resource-link'; @@ -1168,23 +1166,24 @@ export const ClusterSettingsPage: FC = () => { const { t } = useTranslation(); const hasClusterAutoscaler = useFlag(FLAGS.CLUSTER_AUTOSCALER); const title = t('public~Cluster Settings'); - const resources: FirehoseResource[] = [ - { - kind: clusterVersionReference, - name: 'version', - isList: false, - prop: 'obj', - }, - ]; - if (hasClusterAutoscaler) { - resources.push({ - kind: clusterAutoscalerReference, - isList: true, - prop: 'autoscalers', - optional: true, - }); - } - const resourceKeys = _.map(resources, 'prop'); + + const [objData, objLoaded, objLoadError] = useK8sWatchResource({ + kind: clusterVersionReference, + name: 'version', + }); + + const [autoscalersData, autoscalersLoaded, autoscalersLoadError] = useK8sWatchResource< + K8sResourceKind[] + >( + hasClusterAutoscaler + ? { + kind: clusterAutoscalerReference, + isList: true, + } + : null, + ); + + const resourceKeys = hasClusterAutoscaler ? ['obj', 'autoscalers'] : ['obj']; const pages = [ { href: '', @@ -1209,12 +1208,25 @@ export const ClusterSettingsPage: FC = () => { telemetryPrefix: 'Cluster Settings', titlePrefix: title, }; + + const loaded = hasClusterAutoscaler ? objLoaded && autoscalersLoaded : objLoaded; + const loadError = objLoadError || autoscalersLoadError; + + const horizontalNavProps = { + pages, + resourceKeys, + obj: { data: objData, loaded: objLoaded }, + ...(hasClusterAutoscaler && { + autoscalers: { data: autoscalersData, loaded: autoscalersLoaded }, + }), + loaded, + loadError, + }; + return ( {title}} /> - - - + ); }; diff --git a/frontend/public/components/command-line-tools.tsx b/frontend/public/components/command-line-tools.tsx index 0f4a5a234ee..ca1ece2168e 100644 --- a/frontend/public/components/command-line-tools.tsx +++ b/frontend/public/components/command-line-tools.tsx @@ -10,13 +10,22 @@ import { useCopyLoginCommands } from '@console/shared/src/hooks/useCopyLoginComm import SecondaryHeading from '@console/shared/src/components/heading/SecondaryHeading'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; -import { Firehose } from './utils/firehose'; -import { FirehoseResult } from './utils/types'; -import { connectToFlags } from '../reducers/connectToFlags'; +import { useFlag } from '@console/shared/src/hooks/flag'; import { ConsoleCLIDownloadModel } from '../models'; import { referenceForModel } from '../module/k8s'; import { SyncMarkdownView } from './markdown-view'; import { useCopyCodeModal } from '@console/shared/src/hooks/useCopyCodeModal'; +import { useK8sWatchResource } from './utils/k8s-watch-hook'; +import type { K8sResourceCommon } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; +import { LoadingBox } from './utils/status-box'; + +type CLIDownload = K8sResourceCommon & { + spec: { + displayName: string; + description?: string; + links: { href: string; text?: string }[]; + }; +}; export const CommandLineTools: FC = ({ obj }) => { const { t } = useTranslation(); @@ -26,7 +35,7 @@ export const CommandLineTools: FC = ({ obj }) => { externalLoginCommand, ); const showCopyLoginCommand = requestTokenURL || externalLoginCommand; - const data = _.sortBy(_.get(obj, 'data'), 'spec.displayName'); + const data = _.sortBy(obj.data, 'spec.displayName'); const cliData = _.remove(data, (item) => item.metadata.name === 'oc-cli-downloads'); const additionalCommandLineTools = _.map(cliData.concat(data), (tool, index) => { @@ -82,26 +91,35 @@ export const CommandLineTools: FC = ({ obj }) => { ); }; -export const CommandLineToolsPage = connectToFlags(FLAGS.CONSOLE_CLI_DOWNLOAD)( - ({ flags, ...props }) => { - const resources = flags[FLAGS.CONSOLE_CLI_DOWNLOAD] - ? [ - { - kind: referenceForModel(ConsoleCLIDownloadModel), - isList: true, - prop: 'obj', - }, - ] - : []; +export const CommandLineToolsPage = () => { + const { t } = useTranslation(); + const shouldFetch = useFlag(FLAGS.CONSOLE_CLI_DOWNLOAD); + const [cliDownloads, loaded, loadError] = useK8sWatchResource( + shouldFetch + ? { + kind: referenceForModel(ConsoleCLIDownloadModel), + isList: true, + } + : null, + ); + if (!loaded && !loadError) { return ( - - - + <> + {t('public~Command Line Tools')} + + + ); - }, -); + } + + return ; +}; type CommandLineToolsProps = { - obj: FirehoseResult; + obj: { + data: CLIDownload[]; + loaded: boolean; + loadError?: unknown; + }; }; diff --git a/frontend/public/components/console-notifier.tsx b/frontend/public/components/console-notifier.tsx index 575b840d5f2..d78bfe11b8a 100644 --- a/frontend/public/components/console-notifier.tsx +++ b/frontend/public/components/console-notifier.tsx @@ -1,30 +1,54 @@ import type { FC } from 'react'; -import * as _ from 'lodash'; import { ExternalLink } from '@console/shared/src/components/links/ExternalLink'; import { Banner, Flex } from '@patternfly/react-core'; import { FLAGS } from '@console/shared/src/constants/common'; -import { connectToFlags, WithFlagsProps } from '../reducers/connectToFlags'; -import { Firehose } from './utils/firehose'; -import { FirehoseResult } from './utils/types'; +import { useFlag } from '@console/shared/src/hooks/flag'; import { referenceForModel } from '../module/k8s'; import { ConsoleNotificationModel } from '../models/index'; +import { useK8sWatchResource } from './utils/k8s-watch-hook'; +import type { K8sResourceCommon } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; + +type ConsoleNotification = K8sResourceCommon & { + spec: { + location?: 'BannerTop' | 'BannerBottom' | 'BannerTopBottom'; + backgroundColor?: string; + color?: string; + text: string; + link?: { + href: string; + text?: string; + }; + }; +}; type ConsoleNotifierProps = { location: 'BannerTop' | 'BannerBottom' | 'BannerTopBottom'; }; -type PrivateConsoleNotifierProps = ConsoleNotifierProps & { - obj: FirehoseResult; -}; +export const ConsoleNotifier: FC = ({ location }) => { + const shouldFetch = useFlag(FLAGS.CONSOLE_NOTIFICATION); + const [notifications, loaded, loadError] = useK8sWatchResource( + shouldFetch + ? { + kind: referenceForModel(ConsoleNotificationModel), + isList: true, + } + : null, + ); + + if (loadError) { + // eslint-disable-next-line no-console + console.error('Error loading console notifications:', loadError); + return null; + } -const ConsoleNotifier_: FC = ({ obj, location }) => { - if (_.isEmpty(obj)) { + if (!loaded || !notifications?.length) { return null; } return ( <> - {_.map(_.get(obj, 'data'), (notification) => + {notifications.map((notification) => notification.spec.location === location || notification.spec.location === 'BannerTopBottom' || // notification.spec.location is optional @@ -41,7 +65,7 @@ const ConsoleNotifier_: FC = ({ obj, location }) =>

{notification.spec.text}{' '} - {_.get(notification.spec, ['link', 'href']) && ( + {notification.spec.link?.href && ( = ({ obj, location }) => ); }; -ConsoleNotifier_.displayName = 'ConsoleNotifier_'; - -export const ConsoleNotifier = connectToFlags( - FLAGS.CONSOLE_NOTIFICATION, -)(({ flags, ...props }) => { - const resources = flags[FLAGS.CONSOLE_NOTIFICATION] - ? [ - { - kind: referenceForModel(ConsoleNotificationModel), - isList: true, - prop: 'obj', - }, - ] - : []; - return ( - - - - ); -}); ConsoleNotifier.displayName = 'ConsoleNotifier'; diff --git a/frontend/public/components/container.tsx b/frontend/public/components/container.tsx index 5b799742d74..324d9a6333f 100644 --- a/frontend/public/components/container.tsx +++ b/frontend/public/components/container.tsx @@ -35,7 +35,6 @@ import { getContainerStatus, getPullPolicyLabel, } from '../module/k8s/container'; -import { Firehose } from './utils/firehose'; import { HorizontalNav } from './utils/horizontal-nav'; import { ConsoleEmptyState, LoadingBox } from './utils/status-box'; import { NodeLink, resourcePath, ResourceLink } from './utils/resource-link'; @@ -46,6 +45,7 @@ import { getBreadcrumbPath } from '@console/internal/components/utils/breadcrumb import i18n from 'i18next'; import { ErrorPage404 } from './error'; import { ContainerLastState } from './pod'; +import { useK8sWatchResource } from './utils/k8s-watch-hook'; const formatComputeResources = (resources: ResourceList) => _.map(resources, (v, k) => `${k}: ${v}`).join(', '); @@ -460,21 +460,14 @@ ContainerDetailsList.displayName = 'ContainerDetailsList'; export const ContainersDetailsPage: FC = (props) => { const params = useParams(); - return ( - - - - ); + const [pod, loaded, loadError] = useK8sWatchResource({ + name: params.podName, + namespace: params.ns, + kind: 'Pod', + isList: false, + }); + + return ; }; ContainersDetailsPage.displayName = 'ContainersDetailsPage'; @@ -494,7 +487,7 @@ export const ContainerDetails: FC = (props) => { return ; } - const pod = props.obj.data; + const pod = props.pod; const container = getContainer(pod, params.name); if (!container) { @@ -517,12 +510,12 @@ export const ContainerDetails: FC = (props) => { }, { name: t('public~Container details'), path: location.pathname }, ]} - obj={props.obj} + obj={{ data: props.pod, loaded: props.loaded, loadError: props.loadError }} /> ); @@ -555,6 +548,7 @@ export type ContainerDetailsListProps = { }; export type ContainerDetailsProps = { - obj?: any; + pod?: PodKind; loaded?: boolean; + loadError?: unknown; }; diff --git a/frontend/public/components/create-yaml.tsx b/frontend/public/components/create-yaml.tsx index 160ded1417c..7f6320a3134 100644 --- a/frontend/public/components/create-yaml.tsx +++ b/frontend/public/components/create-yaml.tsx @@ -10,8 +10,8 @@ import { import { getYAMLTemplates } from '../models/yaml-templates'; import { connectToPlural } from '../kinds'; import { AsyncComponent } from './utils/async'; -import { Firehose } from './utils/firehose'; import { LoadingBox } from './utils/status-box'; +import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { K8sKind, apiVersionForModel, @@ -119,30 +119,28 @@ export const CreateYAML = (props) => { export const EditYAMLPage: FC = (props) => { const params = useParams(); - const Wrapper = (wrapperProps) => ( + const [obj, loaded, loadError] = useK8sWatchResource({ + kind: props.kind, + name: params.name, + namespace: params.ns, + }); + + if (!loaded && !loadError) { + return ; + } + + if (loadError) { + return ; + } + + return ( import('./edit-yaml').then((c) => c.EditYAML)} create={false} /> ); - return ( - - - - ); }; export type CreateYAMLProps = { diff --git a/frontend/public/components/cron-job.tsx b/frontend/public/components/cron-job.tsx index fce012c7971..7a4c462e7de 100644 --- a/frontend/public/components/cron-job.tsx +++ b/frontend/public/components/cron-job.tsx @@ -24,9 +24,8 @@ import { } from '../module/k8s'; import { ContainerTable } from './utils/container-table'; import { DetailsItem } from './utils/details-item'; -import { Firehose } from './utils/firehose'; -import { FirehoseResourcesResult } from './utils/types'; import { ResourceLink } from './utils/resource-link'; +import { useK8sWatchResources } from './utils/k8s-watch-hook'; import { ResourceSummary } from './utils/details-page'; import { SectionHeading } from './utils/headings'; import { navFactory } from './utils/horizontal-nav'; @@ -216,63 +215,63 @@ export type CronJobPodsComponentProps = { obj: K8sResourceKind; }; -const getJobsWatcher = (namespace: string) => { - return [ - { - prop: 'jobs', +export const CronJobPodsComponent: FC = ({ obj }) => { + const { t } = useTranslation(); + const podFilters = useMemo(() => getPodFilters(t), [t]); + + const resources = useK8sWatchResources<{ + jobs: K8sResourceCommon[]; + pods: PodKind[]; + }>({ + jobs: { isList: true, kind: 'Job', - namespace, + namespace: obj.metadata.namespace, }, - ]; -}; - -const getPodsWatcher = (namespace: string) => { - return [ - ...getJobsWatcher(namespace), - { - prop: 'pods', + pods: { isList: true, kind: 'Pod', - namespace, + namespace: obj.metadata.namespace, }, - ]; -}; + }); + + const loaded = resources.jobs.loaded && resources.pods.loaded; + const loadError = resources.jobs.loadError || resources.pods.loadError; + + const flattenedPods = useMemo(() => { + if (!loaded) { + return []; + } + const jobsData = resources.jobs.data ?? []; + const podsData = resources.pods.data ?? []; + + const jobs = jobsData.filter((job) => + job.metadata?.ownerReferences?.find((ref) => ref.uid === obj.metadata.uid), + ); + + return jobs.reduce((acc, job) => { + acc.push( + ...getPodsForResource(job, { + jobs: { data: jobsData, loaded: true }, + pods: { data: podsData, loaded: true }, + }), + ); + return acc; + }, [] as PodKind[]); + }, [resources.jobs.data, resources.pods.data, obj.metadata.uid, loaded]); -export const CronJobPodsComponent: FC = ({ obj }) => { - const { t } = useTranslation(); - const podFilters = useMemo(() => getPodFilters(t), [t]); return ( - - , - ) => { - if (!_resources.jobs.loaded || !_resources.pods.loaded) { - return []; - } - const jobs = _resources.jobs.data.filter((job) => - job.metadata?.ownerReferences?.find((ref) => ref.uid === obj.metadata.uid), - ); - return ( - jobs && - jobs.reduce((acc, job) => { - acc.push(...getPodsForResource(job, _resources)); - return acc; - }, []) - ); - }} - kinds={['Pods']} - ListComponent={PodList} - rowFilters={podFilters} - hideColumnManagement={true} - omitFilterToolbar={true} - /> - + flattenedPods} + kinds={['Pods']} + ListComponent={PodList} + rowFilters={podFilters} + hideColumnManagement={true} + omitFilterToolbar={true} + loaded={loaded} + loadError={loadError} + /> ); }; @@ -281,26 +280,42 @@ export type CronJobJobsComponentProps = { obj: K8sResourceKind; }; -export const CronJobJobsComponent: FC = ({ obj }) => ( - - +export const CronJobJobsComponent: FC = ({ obj }) => { + const resources = useK8sWatchResources<{ + jobs: K8sResourceCommon[]; + }>({ + jobs: { + isList: true, + kind: 'Job', + namespace: obj.metadata.namespace, + }, + }); + + const { loaded, loadError } = resources.jobs; + + const flattenedJobs = useMemo(() => { + if (!loaded) { + return []; + } + return (resources.jobs.data ?? []).filter((job) => + job.metadata?.ownerReferences?.find((ref) => ref.uid === obj.metadata.uid), + ); + }, [resources.jobs.data, obj.metadata.uid, loaded]); + + return ( + ) => { - if (!_resources.jobs.loaded) { - return []; - } - return _resources.jobs.data.filter((job) => - job.metadata?.ownerReferences?.find((ref) => ref.uid === obj.metadata.uid), - ); - }} + flatten={() => flattenedJobs} kinds={['Jobs']} ListComponent={JobsList} hideColumnManagement={true} omitFilterToolbar={true} + loaded={loaded} + loadError={loadError} /> - - -); + + ); +}; const useCronJobsColumns = (): TableColumn[] => { const { t } = useTranslation(); diff --git a/frontend/public/components/edit-yaml.tsx b/frontend/public/components/edit-yaml.tsx index 22471bf04e3..12828e9e71c 100644 --- a/frontend/public/components/edit-yaml.tsx +++ b/frontend/public/components/edit-yaml.tsx @@ -30,15 +30,15 @@ import { K8sResourceKind, } from '@console/dynamic-plugin-sdk'; import { useResolvedExtensions } from '@console/dynamic-plugin-sdk/src/api/useResolvedExtensions'; -import { connectToFlags, WithFlagsProps } from '../reducers/connectToFlags'; +import { useFlag } from '@console/shared/src/hooks/flag'; import { LazyManagedResourceSaveModalOverlay } from './modals'; import ReplaceCodeModal from './modals/replace-code-modal'; import { checkAccess } from './utils/rbac'; -import { Firehose } from './utils/firehose'; import { Loading } from './utils/status-box'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { resourceObjPath, resourceListPathFromModel } from './utils/resource-link'; -import { FirehoseResult } from './utils/types'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; +import type { CodeEditorProps } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; import { referenceForModel, k8sCreate, @@ -66,7 +66,6 @@ import { RootState } from '@console/internal/redux'; import { getActiveNamespace } from '@console/internal/reducers/ui'; import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { ErrorModal } from './modals/error-modal'; -import type { CodeEditorProps } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; const generateObjToLoad = ( templateExtensions: Parameters[0], @@ -96,9 +95,7 @@ export interface EditYAMLProps { /** Whether to allow multiple YAML documents in the editor */ allowMultiple?: boolean; /** Whether this is a create operation */ - create?: boolean; - /** List of YAML samples to display */ - yamlSamplesList?: FirehoseResult; + create: boolean; /** Custom CSS class for the editor */ customClass?: string; /** Callback function to handle changes in the YAML content */ @@ -144,8 +141,7 @@ type EditYAMLInnerProps = ReturnType & EditYAMLProps; const EditYAMLInner: FC = (props) => { const { allowMultiple, - create = false, - yamlSamplesList, + create, customClass, onChange = () => null, models, @@ -160,6 +156,22 @@ const EditYAMLInner: FC = (props) => { } = props; const navigate = useNavigate(); + const hasYAMLSampleFlag = useFlag(FLAGS.CONSOLE_YAML_SAMPLE); + + const watchResources = { + yamlSamplesList: hasYAMLSampleFlag + ? { + kind: referenceForModel(ConsoleYAMLSampleModel), + isList: true, + } + : null, + }; + + const resources = useK8sWatchResources<{ + yamlSamplesList: K8sResourceCommon[]; + }>(watchResources); + + const yamlSamplesList = resources.yamlSamplesList; const fireTelemetryEvent = useTelemetry(); const postFormSubmissionCallback = useResourceConnectionHandler(); const [errors, setErrors] = useState(null); @@ -980,24 +992,4 @@ const EditYAMLInner: FC = (props) => { * This component loads the entire Monaco editor library with it. * Consider using `AsyncComponent` to dynamically load this component when needed. */ -export const EditYAML_ = connect(stateToProps)(EditYAMLInner); - -export const EditYAML = connectToFlags(FLAGS.CONSOLE_YAML_SAMPLE)( - ({ flags, ...props }) => { - const resources = flags[FLAGS.CONSOLE_YAML_SAMPLE] - ? [ - { - kind: referenceForModel(ConsoleYAMLSampleModel), - isList: true, - prop: 'yamlSamplesList', - }, - ] - : []; - - return ( - - - - ); - }, -); +export const EditYAML = connect(stateToProps)(EditYAMLInner); diff --git a/frontend/public/components/factory/__tests__/details.spec.tsx b/frontend/public/components/factory/__tests__/details.spec.tsx index d155e8e98a7..c433061cf26 100644 --- a/frontend/public/components/factory/__tests__/details.spec.tsx +++ b/frontend/public/components/factory/__tests__/details.spec.tsx @@ -1,19 +1,17 @@ import { screen, act } from '@testing-library/react'; - import { DetailsPage } from '@console/internal/components/factory/details'; import { PodModel } from '@console/internal/models'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; import { K8sModel } from '@console/dynamic-plugin-sdk/src/api/common-types'; +import * as k8sWatchHook from '@console/internal/components/utils/k8s-watch-hook'; -let capturedFirehoseProps = null; - -jest.mock('@console/internal/components/utils/firehose', () => ({ - Firehose: (props) => { - capturedFirehoseProps = props; - return props.children; - }, +jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ + useK8sWatchResources: jest.fn(() => ({})), + useK8sWatchResource: jest.fn(() => [null, true, null]), })); +const mockUseK8sWatchResources = k8sWatchHook.useK8sWatchResources as jest.Mock; + const MockPageComponent = () =>

Mock Page Content
; const createMockPages = () => [ @@ -34,7 +32,8 @@ const defaultProps = { describe('Resource DetailsPage', () => { beforeEach(() => { - capturedFirehoseProps = null; + mockUseK8sWatchResources.mockClear(); + mockUseK8sWatchResources.mockReturnValue({ obj: { data: null, loaded: false } }); }); it('should verify the detail page basic information and navigation tabs', async () => { @@ -42,8 +41,11 @@ describe('Resource DetailsPage', () => { renderWithProviders(); }); - // Verify Firehose receives the expected resources - expect(capturedFirehoseProps.resources).toHaveLength(1); + // Verify hook was called + expect(mockUseK8sWatchResources).toHaveBeenCalled(); + const watchConfig = mockUseK8sWatchResources.mock.calls[0]?.[0] as any; + expect(watchConfig?.obj).toBeDefined(); + expect(watchConfig?.obj?.kind).toBe('Pod'); // Verify resource information is displayed expect(screen.getByText('Pod')).toBeVisible(); @@ -55,7 +57,7 @@ describe('Resource DetailsPage', () => { expect(screen.getByRole('tab', { name: 'Events' })).toBeVisible(); }); - it('should verify details page with extra resources passed to Firehose', async () => { + it('should verify details page with extra resources passed to useK8sWatchResources', async () => { const extraResource = [ { kind: 'ConfigMap', @@ -65,12 +67,21 @@ describe('Resource DetailsPage', () => { prop: 'configMap', }, ]; + mockUseK8sWatchResources.mockReturnValue({ + obj: { data: null, loaded: false }, + configMap: { data: null, loaded: false }, + }); + await act(async () => { renderWithProviders(); }); + // Verify hook was called with both resources + expect(mockUseK8sWatchResources).toHaveBeenCalled(); + const watchConfig = mockUseK8sWatchResources.mock.calls[0]?.[0] as any; + // Verify total resources count (1 from defaultProps + 1 extra) - expect(capturedFirehoseProps.resources).toHaveLength(2); + expect(Object.keys(watchConfig || {})).toHaveLength(2); // Verify basic UI elements are still present expect(screen.getByText('example-pod')).toBeVisible(); @@ -79,15 +90,29 @@ describe('Resource DetailsPage', () => { expect(screen.getByRole('tab', { name: 'Events' })).toBeVisible(); // Verify Pod resource from DetailsPage props - expect(capturedFirehoseProps.resources[0]).toEqual({ + expect(watchConfig?.obj).toEqual({ kind: defaultProps.kind, name: defaultProps.name, namespace: defaultProps.namespace, isList: false, - prop: 'obj', + selector: undefined, + fieldSelector: undefined, + limit: undefined, + namespaced: undefined, + optional: undefined, }); // Verify extra ConfigMap resource from DetailsPage props - expect(capturedFirehoseProps.resources[1]).toEqual(extraResource[0]); + expect(watchConfig?.configMap).toEqual({ + kind: 'ConfigMap', + name: 'example-configmap', + namespace: 'example-namespace', + isList: false, + selector: undefined, + fieldSelector: undefined, + limit: undefined, + namespaced: undefined, + optional: undefined, + }); }); }); diff --git a/frontend/public/components/factory/__tests__/list-page.spec.tsx b/frontend/public/components/factory/__tests__/list-page.spec.tsx index 61385e3af93..6f061fb1e2f 100644 --- a/frontend/public/components/factory/__tests__/list-page.spec.tsx +++ b/frontend/public/components/factory/__tests__/list-page.spec.tsx @@ -1,5 +1,4 @@ import { screen, fireEvent } from '@testing-library/react'; - import { TextFilter } from '@console/internal/components/factory/text-filter'; import { ListPageWrapper, @@ -7,16 +6,15 @@ import { MultiListPage, } from '@console/internal/components/factory/list-page'; import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; +import * as k8sWatchHook from '@console/internal/components/utils/k8s-watch-hook'; -let capturedFirehoseProps = null; - -jest.mock('@console/internal/components/utils/firehose', () => ({ - Firehose: (props) => { - capturedFirehoseProps = props; - return props.children; - }, +jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ + useK8sWatchResources: jest.fn(() => ({})), + useK8sWatchResource: jest.fn(() => [null, true, null]), })); +const mockUseK8sWatchResources = k8sWatchHook.useK8sWatchResources as jest.Mock; + describe('TextFilter component', () => { it('renders text input without label', () => { const onChange = jest.fn(); @@ -152,11 +150,12 @@ describe('ListPageWrapper component', () => { describe(' MultiListPage component', () => { beforeEach(() => { - capturedFirehoseProps = null; + mockUseK8sWatchResources.mockClear(); }); - it('renders with Firehose wrapper and displays ListComponent content', () => { + it('renders with useK8sWatchResources hook and displays ListComponent content', () => { const ListComponent = () =>
Multi List
; + mockUseK8sWatchResources.mockReturnValue({}); renderWithProviders( { ); expect(screen.getByText('Multi List')).toBeVisible(); - expect(capturedFirehoseProps.resources).toHaveLength(1); - expect(capturedFirehoseProps.resources[0].kind).toBe('Pod'); - expect(capturedFirehoseProps.resources[0].name).toBe('example-pod'); + expect(mockUseK8sWatchResources).toHaveBeenCalled(); + const watchConfig = mockUseK8sWatchResources.mock.calls[0]?.[0] as any; + expect(watchConfig?.Pod).toBeDefined(); + expect(watchConfig?.Pod?.kind).toBe('Pod'); + expect(watchConfig?.Pod?.name).toBe('example-pod'); }); }); diff --git a/frontend/public/components/factory/details.tsx b/frontend/public/components/factory/details.tsx index 5026e72d42a..bbe218a7b0e 100644 --- a/frontend/public/components/factory/details.tsx +++ b/frontend/public/components/factory/details.tsx @@ -22,9 +22,11 @@ import { FirehoseResult, K8sResourceKindReference, K8sResourceKind, + K8sResourceCommon, + WatchK8sResource, } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; -import { Firehose } from '../utils/firehose'; import { HorizontalNav } from '../utils/horizontal-nav'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import type { Page } from '../utils/horizontal-nav'; import { ConnectedPageHeading, @@ -101,18 +103,48 @@ export const DetailsPage = withFallback(({ pages = [], ...prop }, []); let allPages = [...pages, ...pluginPages]; allPages = allPages.length ? allPages : null; - const objResource: FirehoseResource = { - kind: props.kind, - name: props.name, - namespace: props.namespace, - isList: false, - prop: 'obj', - }; + const objResource = useMemo( + () => ({ + kind: props.kind, + name: props.name, + namespace: props.namespace, + isList: false, + prop: 'obj', + }), + [props.kind, props.name, props.namespace], + ); + const titleProviderValues = { telemetryPrefix: props?.kindObj?.kind, titlePrefix: `${props.name} · ${getTitleForNodeKind(props?.kindObj?.kind)}`, }; + // Build resources to watch + const watchResources = useMemo(() => { + const allResources = [...(_.isNil(props.obj) ? [objResource] : []), ...(props.resources ?? [])]; + return allResources.reduce((acc, r) => { + const key = r.prop || r.kind; + acc[key] = { + kind: r.kind, + name: r.name, + namespace: r.namespace, + isList: r.isList, + selector: r.selector, + fieldSelector: r.fieldSelector, + limit: r.limit, + namespaced: r.namespaced, + optional: r.optional, + }; + return acc; + }, {} as Record); + }, [props.obj, props.resources, objResource]); + + const watchedResources = useK8sWatchResources< + Record + >(watchResources); + + const objData = _.isNil(props.obj) ? watchedResources.obj : props.obj; + return ( {resolvedBreadcrumbExtension && ( @@ -124,42 +156,40 @@ export const DetailsPage = withFallback(({ pages = [], ...prop /> )} - - - - + + ); }, ErrorBoundaryFallbackPage); diff --git a/frontend/public/components/factory/list-page.tsx b/frontend/public/components/factory/list-page.tsx index 230883d3e62..c210c5b75a1 100644 --- a/frontend/public/components/factory/list-page.tsx +++ b/frontend/public/components/factory/list-page.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as _ from 'lodash'; import type { ComponentType, FC, ReactNode } from 'react'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { useParams, useNavigate } from 'react-router-dom-v5-compat'; import { Button, Grid, GridItem } from '@patternfly/react-core'; @@ -13,6 +13,8 @@ import ErrorBoundaryFallbackPage from '@console/shared/src/components/error/fall import { ColumnLayout, K8sResourceCommon, + WatchK8sResource, + WatchK8sResultsObject, } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; import { filterList } from '@console/dynamic-plugin-sdk/src/app/k8s/actions/k8s'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; @@ -21,13 +23,8 @@ import { ErrorPage404 } from '../error'; import { K8sKind } from '../../module/k8s/types'; import { getReferenceForModel as referenceForModel } from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-ref'; import { Selector } from '@console/dynamic-plugin-sdk/src/api/common-types'; -import { Firehose } from '../utils/firehose'; -import { - FirehoseResource, - FirehoseResourcesResult, - FirehoseResultObject, - FirehoseResult, -} from '../utils/types'; +import { useK8sWatchResources } from '../utils/k8s-watch-hook'; +import { FirehoseResource, FirehoseResourcesResult, FirehoseResultObject } from '../utils/types'; import { inject, kindObj } from '../utils/inject'; import { makeQuery, @@ -62,7 +59,12 @@ type ListPageWrapperProps = { hideLabelFilter?: boolean; columnLayout?: ColumnLayout; name?: string; + /** @deprecated - use watchedResources instead */ resources?: FirehoseResourcesResult; + /** Resources fetched via useK8sWatchResources */ + watchedResources?: Record>; + loaded?: boolean; + loadError?: unknown; reduxIDs?: string[]; textFilter?: string; nameFilterPlaceholder?: string; @@ -90,6 +92,7 @@ export const ListPageWrapper: FC = (props) => { columnLayout, name, resources, + watchedResources, nameFilter, omitFilterToolbar, } = props; @@ -102,7 +105,10 @@ export const ListPageWrapper: FC = (props) => { } }, [dispatch, nameFilter, memoizedIds]); - const data = flatten ? flatten(resources) : []; + // TODO: Remove the resources prop and the fallback ?? resources after all components are migrated from Firehose to hooks. + // Use watchedResources (from useK8sWatchResources) if available, fallback to resources (from Firehose) + const resourceData = watchedResources ?? resources; + const data = flatten ? flatten(resourceData) : []; const Filter = ( ((props) => { hideColumnManagement, columnLayout, omitFilterToolbar, - flatten = (_resources) => _.get(_resources, name || kind, {} as FirehoseResult).data, + flatten = (_resources) => + (_resources[name || kind] ?? ({} as WatchK8sResultsObject)).data, } = props; const { t } = useTranslation(); const params = useParams(); @@ -510,12 +517,55 @@ export const MultiListPage: FC = (props) => { } = props; const { t } = useTranslation(); - const resources = _.map(props.resources, (r) => ({ - ...r, - isList: r.isList !== undefined ? r.isList : true, - namespace: r.namespaced ? namespace : r.namespace, - prop: r.prop || r.kind, - })); + + // Build resources configuration for FireMan (needs prop for redux IDs) + const k8sResources = useMemo( + () => + _.map(props.resources, (r) => ({ + ...r, + isList: r.isList !== undefined ? r.isList : true, + namespace: r.namespaced ? namespace : r.namespace, + prop: r.prop || r.kind, + })), + [props.resources, namespace], + ); + + // Build watch resources configuration for useK8sWatchResources + const watchResources = useMemo(() => { + if (mock) { + return {}; + } + return k8sResources.reduce((acc, r) => { + const key = r.prop || r.kind; + acc[key] = { + kind: r.kind, + name: r.name, + namespace: r.namespace, + isList: r.isList, + selector: r.selector, + fieldSelector: r.fieldSelector, + limit: r.limit, + namespaced: r.namespaced, + optional: r.optional, + }; + return acc; + }, {} as Record); + }, [k8sResources, mock]); + + const watchedResources = useK8sWatchResources< + Record + >(watchResources); + + // Aggregate individual resource loading states into a single boolean (true when all loaded, excluding errors) + const loaded = useMemo(() => { + const resourceValues = Object.values(watchedResources); + // If we expect resources but haven't received any yet, we're still loading + if (Object.keys(watchResources).length > 0 && resourceValues.length === 0) { + return false; + } + // Check if all resources (excluding errors) are loaded + return resourceValues.filter((r) => !r.loadError).every((r) => r.loaded); + }, [watchedResources, watchResources]); return ( = (props) => { filterLabel={filterLabel || t('public~by name')} helpText={helpText} helpAlert={helpAlert} - resources={mock ? [] : resources} + resources={mock ? [] : k8sResources} textFilter={textFilter} title={showTitle ? title : undefined} badge={badge} > - - - + ); }; diff --git a/frontend/public/components/instantiate-template.tsx b/frontend/public/components/instantiate-template.tsx index 952eeec1320..9f863dfdbc9 100644 --- a/frontend/public/components/instantiate-template.tsx +++ b/frontend/public/components/instantiate-template.tsx @@ -35,9 +35,9 @@ import { normalizeIconClass, } from './catalog/catalog-item-icon'; import { ButtonBar } from './utils/button-bar'; -import { Firehose } from './utils/firehose'; import { LoadError, LoadingBox } from './utils/status-box'; import { NsDropdown } from './utils/list-dropdown'; +import { useK8sWatchResource } from './utils/k8s-watch-hook'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; import { SecretModel, TemplateInstanceModel } from '../models'; import { @@ -327,6 +327,14 @@ export const TemplateForm: FC = (props) => { return ; } + if (!obj.data) { + return ( + + {t('public~Template not found or invalid URL parameters.')} + + ); + } + const template: TemplateKind = obj.data; const params = template.parameters || []; @@ -387,31 +395,34 @@ export const TemplateForm: FC = (props) => { ); }; -export const InstantiateTemplatePage: FC<{}> = (props) => { +export const InstantiateTemplatePage: FC<{}> = () => { const title = 'Instantiate Template'; const location = useLocation(); const searchParams = new URLSearchParams(location.search); const templateName = searchParams.get('template'); const templateNamespace = searchParams.get('template-ns'); const preselectedNamespace = searchParams.get('preselected-ns'); - const resources = [ - { - kind: 'Template', - name: templateName, - namespace: templateNamespace, - isList: false, - prop: 'obj', - }, - ]; + + const [template, loaded, loadError] = useK8sWatchResource( + templateName && templateNamespace + ? { + kind: 'Template', + name: templateName, + namespace: templateNamespace, + isList: false, + } + : null, + ); return ( <> {title} - - - + ); diff --git a/frontend/public/components/monitoring/alertmanager/alertmanager-config.tsx b/frontend/public/components/monitoring/alertmanager/alertmanager-config.tsx index e6a3a73bd69..9e99ec04ca0 100644 --- a/frontend/public/components/monitoring/alertmanager/alertmanager-config.tsx +++ b/frontend/public/components/monitoring/alertmanager/alertmanager-config.tsx @@ -33,10 +33,10 @@ import { K8sResourceKind } from '../../../module/k8s'; import { LazyAlertRoutingModalOverlay } from '../../modals'; import { useWarningModal } from '@console/shared/src/hooks/useWarningModal'; import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; -import { Firehose } from '../../utils/firehose'; import { Kebab } from '../../utils/kebab'; import { SectionHeading } from '../../utils/headings'; import { StatusBox } from '../../utils/status-box'; +import { useK8sWatchResource } from '../../utils/k8s-watch-hook'; import { getAlertmanagerConfig, patchAlertmanagerConfig, @@ -598,6 +598,13 @@ export const AlertmanagerConfig: FC = () => { const breadcrumbs = breadcrumbsForGlobalConfig('Alertmanager', configPath); + const [secret, loaded, loadError] = useK8sWatchResource({ + kind: 'Secret', + name: 'alertmanager-main', + namespace: 'openshift-monitoring', + isList: false, + }); + return ( <> @@ -613,19 +620,7 @@ export const AlertmanagerConfig: FC = () => { }, ]} /> - - - + ); }; diff --git a/frontend/public/components/monitoring/alertmanager/alertmanager-yaml-editor.tsx b/frontend/public/components/monitoring/alertmanager/alertmanager-yaml-editor.tsx index c68b585179c..69521cc2f2c 100644 --- a/frontend/public/components/monitoring/alertmanager/alertmanager-yaml-editor.tsx +++ b/frontend/public/components/monitoring/alertmanager/alertmanager-yaml-editor.tsx @@ -11,9 +11,9 @@ import { breadcrumbsForGlobalConfig } from '../../cluster-settings/global-config import { K8sResourceKind } from '../../../module/k8s'; import { AsyncComponent } from '../../utils/async'; -import { Firehose } from '../../utils/firehose'; import { StatusBox } from '../../utils/status-box'; import { patchAlertmanagerConfig, getAlertmanagerYAML } from './alertmanager-utils'; +import { useK8sWatchResource } from '../../utils/k8s-watch-hook'; const EditAlertmanagerYAML = (props) => ( = () => { const breadcrumbs = breadcrumbsForGlobalConfig('Alertmanager', configPath); + const [secret, loaded, loadError] = useK8sWatchResource({ + kind: 'Secret', + name: 'alertmanager-main', + namespace: 'openshift-monitoring', + isList: false, + }); + return ( <> @@ -142,19 +149,7 @@ export const AlertmanagerYAML: FC<{}> = () => { }, ]} /> - - - + ); }; diff --git a/frontend/public/components/monitoring/receiver-forms/alert-manager-receiver-forms.tsx b/frontend/public/components/monitoring/receiver-forms/alert-manager-receiver-forms.tsx index b6452340257..84ffa1f58fa 100644 --- a/frontend/public/components/monitoring/receiver-forms/alert-manager-receiver-forms.tsx +++ b/frontend/public/components/monitoring/receiver-forms/alert-manager-receiver-forms.tsx @@ -26,8 +26,8 @@ import { safeLoad } from 'js-yaml'; import { APIError } from '@console/shared/src/types/resource'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; import { ButtonBar } from '../../utils/button-bar'; -import { Firehose } from '../../utils/firehose'; import { StatusBox } from '../../utils/status-box'; +import { useK8sWatchResource } from '../../utils/k8s-watch-hook'; import { getAlertmanagerConfig, patchAlertmanagerConfig, @@ -575,36 +575,41 @@ const ReceiverWrapper = memo(({ obj, ...props }) => { ); }); -const resources = [ - { +export const CreateReceiver = () => { + const { t } = useTranslation(); + const [secret, loaded, loadError] = useK8sWatchResource({ kind: 'Secret', name: 'alertmanager-main', namespace: 'openshift-monitoring', isList: false, - prop: 'obj', - }, -]; + }); -export const CreateReceiver = () => { - const { t } = useTranslation(); return ( - - - + ); }; export const EditReceiver = () => { const { t } = useTranslation(); const params = useParams(); + const [secret, loaded, loadError] = useK8sWatchResource({ + kind: 'Secret', + name: 'alertmanager-main', + namespace: 'openshift-monitoring', + isList: false, + }); + return ( - - - + ); }; diff --git a/frontend/public/components/namespace-bar.tsx b/frontend/public/components/namespace-bar.tsx index a1f87f6e62b..dced54c1535 100644 --- a/frontend/public/components/namespace-bar.tsx +++ b/frontend/public/components/namespace-bar.tsx @@ -18,9 +18,12 @@ import { k8sGet } from '@console/internal/module/k8s'; import { setFlag } from '../actions/flags'; import { NamespaceModel, ProjectModel } from '../models'; import { flagPending } from '../reducers/features'; -import { Firehose } from './utils/firehose'; -import { FirehoseResult } from './utils/types'; import { useQueryParamsMutator } from './utils/router'; +import { useK8sWatchResource } from './utils/k8s-watch-hook'; +import type { + WatchK8sResultsObject, + K8sResourceCommon, +} from '@console/dynamic-plugin-sdk/src/extensions/console-types'; import { useCreateNamespaceOrProjectModal } from '@console/shared/src/hooks/useCreateNamespaceOrProjectModal'; import type { RootState } from '../redux'; import { setActiveApplication } from '../actions/ui'; @@ -28,7 +31,7 @@ import { setActiveApplication } from '../actions/ui'; export type NamespaceBarDropdownsProps = { children: ReactNode; isDisabled: boolean; - namespace?: FirehoseResult; + namespace?: WatchK8sResultsObject; onNamespaceChange: (namespace: string) => void; useProjects: boolean; }; @@ -50,10 +53,10 @@ export const NamespaceBarDropdowns: FC = ({ const [activeNamespaceError, setActiveNamespaceError] = useState(false); const canListNS = useFlag(FLAGS.CAN_LIST_NS); useEffect(() => { - if (namespace.loaded) { + if (namespace?.loaded) { dispatch(setFlag(FLAGS.SHOW_OPENSHIFT_START_GUIDE, _.isEmpty(namespace.data))); } - }, [dispatch, namespace.data, namespace.loaded]); + }, [dispatch, namespace?.data, namespace?.loaded]); /* Check if the activeNamespace is present in the cluster */ useEffect(() => { @@ -124,6 +127,16 @@ export const NamespaceBar: FC = const useProjects = useSelector(({ k8s }) => k8s.hasIn(['RESOURCES', 'models', ProjectModel.kind]), ); + + const [namespaces, loaded, loadError] = useK8sWatchResource( + hideProjects + ? null + : { + kind: getModel(useProjects).kind, + isList: true, + }, + ); + return (
{hideProjects ? ( @@ -131,20 +144,17 @@ export const NamespaceBar: FC = {children}
) : ( - // Data from Firehose is not used directly by the NamespaceDropdown nor the children. + // Data from useK8sWatchResource is not used directly by the NamespaceDropdown nor the children. // Data is used to determine if the StartGuide should be shown. // See NamespaceBarDropdowns_ above. - - - {children} - - + {children} + )} ); diff --git a/frontend/public/components/storage-class-form.tsx b/frontend/public/components/storage-class-form.tsx index 4727babec07..3c1261ca014 100755 --- a/frontend/public/components/storage-class-form.tsx +++ b/frontend/public/components/storage-class-form.tsx @@ -24,9 +24,9 @@ import { PageHeading } from '@console/shared/src/components/heading/PageHeading' import { ConsoleSelect } from '@console/internal/components/utils/console-select'; import { AsyncComponent } from './utils/async'; import { ButtonBar } from './utils/button-bar'; -import { Firehose } from './utils/firehose'; -import type { FirehoseResult } from './utils/types'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; import { NameValueEditorPair } from './utils/types'; +import { useK8sWatchResources } from './utils/k8s-watch-hook'; import { resourceObjPath } from './utils/resource-link'; import { ExternalLink } from '@console/shared/src/components/links/ExternalLink'; import { k8sCreate, K8sResourceKind, referenceForModel, referenceFor } from './../module/k8s'; @@ -734,7 +734,7 @@ const mapDispatchToProps = (): DispatchProps => ({ export type StorageClassFormProps = StateProps & DispatchProps & { resources?: { - [key: string]: FirehoseResult; + [key: string]: WatchK8sResultsObject; }; } & { extensions?: [ResolvedExtension[], boolean, any[]]; @@ -782,14 +782,27 @@ export const ConnectedStorageClassForm = connect( }); export const StorageClassForm = (props) => { - const resources = [ - { kind: StorageClassModel.kind, isList: true, prop: 'sc' }, - { kind: referenceForModel(CSIDriverModel), isList: true, prop: 'csi' }, - ]; + const watchedResources = useK8sWatchResources({ + sc: { kind: StorageClassModel.kind, isList: true }, + csi: { kind: referenceForModel(CSIDriverModel), isList: true }, + }); + return ( - - - + ); }; diff --git a/frontend/public/components/utils/horizontal-nav.tsx b/frontend/public/components/utils/horizontal-nav.tsx index dc7c812ceed..5acdc4e948e 100644 --- a/frontend/public/components/utils/horizontal-nav.tsx +++ b/frontend/public/components/utils/horizontal-nav.tsx @@ -37,12 +37,17 @@ import { import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; export const editYamlComponent = (props) => ( - import('../edit-yaml').then((c) => c.EditYAML)} obj={props.obj} /> + import('../edit-yaml').then((c) => c.EditYAML)} + obj={props.obj} + create={false} + /> ); export const viewYamlComponent = (props) => ( import('../edit-yaml').then((c) => c.EditYAML)} obj={props.obj} + create={false} readOnly={true} /> ); diff --git a/frontend/public/components/utils/list-dropdown.tsx b/frontend/public/components/utils/list-dropdown.tsx index 96d9d2d75fd..94d34fd68d6 100644 --- a/frontend/public/components/utils/list-dropdown.tsx +++ b/frontend/public/components/utils/list-dropdown.tsx @@ -6,8 +6,8 @@ import { Alert } from '@patternfly/react-core'; import { useFlag } from '@console/shared/src/hooks/flag'; import { FLAGS } from '@console/shared/src/constants'; import { ActionItem, ConsoleSelect } from '@console/internal/components/utils/console-select'; -import { Firehose } from './firehose'; import { LoadingInline } from './status-box'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { ResourceName } from './resource-icon'; import { flagPending } from '../../reducers/features'; import { NamespaceModel, ProjectModel } from '@console/internal/models'; @@ -19,6 +19,7 @@ import { K8sResourceCommon, K8sModel, K8sResourceKind, + WatchK8sResource, } from '@console/dynamic-plugin-sdk/src'; const getKey = (key, keyKind) => { @@ -52,7 +53,11 @@ export interface ListDropdownProps { loadError?: boolean; } -const ListDropdown_: FC = ({ +interface ListDropdownInternalProps extends Omit { + resources?: Record; +} + +const ListDropdown_: FC = ({ desc, placeholder, loaded, @@ -197,13 +202,41 @@ const ListDropdown_: FC = ({ }; export const ListDropdown: FC = (props) => { - const resources = _.map(props.resources, (resource) => - _.assign({ isList: true, prop: resource.kind }, resource), + const watchResources = useMemo(() => { + if (!props.resources || props.resources.length === 0) { + return {}; + } + return props.resources.reduce((acc, resource) => { + // Use prop as key if provided, otherwise fallback to kind (matches original Firehose behavior) + const key = resource.prop || resource.kind; + acc[key] = { + kind: resource.kind, + isList: true, + namespace: resource.namespace, + selector: resource.selector, + fieldSelector: resource.fieldSelector, + limit: resource.limit, + namespaced: resource.namespaced, + optional: resource.optional, + }; + return acc; + }, {} as Record); + }, [props.resources]); + + const watchedResources = useK8sWatchResources>( + watchResources, ); + + const loaded = useMemo(() => { + return Object.values(watchedResources).every((r) => r.loaded); + }, [watchedResources]); + + const loadError = useMemo(() => { + return Object.values(watchedResources).some((r) => r.loadError); + }, [watchedResources]); + return ( - - - + ); }; diff --git a/frontend/public/components/utils/storage-class-dropdown.tsx b/frontend/public/components/utils/storage-class-dropdown.tsx index 689ca958d4e..a4f6e7b0970 100644 --- a/frontend/public/components/utils/storage-class-dropdown.tsx +++ b/frontend/public/components/utils/storage-class-dropdown.tsx @@ -7,8 +7,8 @@ import * as fuzzy from 'fuzzysearch'; import { WithTranslation, withTranslation } from 'react-i18next'; import { ConsoleSelect } from '@console/internal/components/utils/console-select'; -import { Firehose } from './firehose'; import { LoadingInline } from './status-box'; +import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { ResourceName, ResourceIcon } from './resource-icon'; import { isDefaultClass } from '../storage-class'; import { css } from '@patternfly/react-styles'; @@ -212,10 +212,26 @@ export const StorageClassDropdownInner = withTranslation()( ); export const StorageClassDropdown = (props) => { + const [data, loaded, loadError] = useK8sWatchResource({ + kind: 'StorageClass', + isList: true, + }); + + const resources = { + StorageClass: { + data, + loaded, + loadError, + }, + }; + return ( - - - + ); }; diff --git a/frontend/public/locales/en/public.json b/frontend/public/locales/en/public.json index 101bb3fc26c..dfb0a4d277e 100644 --- a/frontend/public/locales/en/public.json +++ b/frontend/public/locales/en/public.json @@ -703,6 +703,7 @@ "Compress to a single line of content. This may strip any new lines you have entered.": "Compress to a single line of content. This may strip any new lines you have entered.", "Expand to enter multiple lines of content. This is required if you need to include newline characters.": "Expand to enter multiple lines of content. This is required if you need to include newline characters.", "Template": "Template", + "Template not found or invalid URL parameters.": "Template not found or invalid URL parameters.", "(generated if empty)": "(generated if empty)", "{{jobsSucceeded}} of {{completions}}": "{{jobsSucceeded}} of {{completions}}", "Job status": "Job status", From 4e718b82d9215f784fb7d641adb5939e4b753795 Mon Sep 17 00:00:00 2001 From: cyril-ui-developer Date: Wed, 4 Mar 2026 08:45:52 -0500 Subject: [PATCH 3/3] Remove Firehose Component and Fix and Cleanup Impacted Types --- .../docs/console-extensions.md | 10 +- .../src/api/internal-types.ts | 2 +- .../src/extensions/console-types.ts | 29 +- .../src/extensions/dashboard-types.ts | 11 +- .../src/extensions/dashboards.ts | 14 +- .../status-card/OperatorStatusBody.tsx | 10 +- .../components/dropdown/ResourceDropdown.tsx | 11 +- .../formik-fields/ResourceDropdownField.tsx | 20 +- .../src/hooks/useResourceSidebarSamples.ts | 13 +- .../packages/console-shared/src/types/pod.ts | 23 +- .../src/utils/__tests__/test-resource-data.ts | 34 +- .../console-shared/src/utils/utils.ts | 4 +- .../components/ImageVulnerabilitiesList.tsx | 7 +- .../__tests__/SecretsSection.spec.tsx | 6 - .../sections/__tests__/SourceSection.spec.tsx | 6 - .../catalog/providers/useTemplates.tsx | 5 +- .../dropdown/SourceSecretDropdown.tsx | 60 +- .../edit-application-types.ts | 18 +- .../health-checks/AddHealthChecksForm.tsx | 4 +- .../src/utils/imagestream-utils.ts | 6 +- .../__tests__/helm-release-mock-data.ts | 6 +- .../details-page/HelmReleaseDetails.tsx | 8 +- .../__tests__/HelmReleaseDetails.spec.tsx | 5 - .../resources/HelmReleaseResources.tsx | 6 +- .../__tests__/HelmReleaseResources.spec.tsx | 2 +- .../__tests__/helm-data-transformer.spec.ts | 4 +- .../views/details-page.ts | 5 +- .../sink-pubsub/SinkPubsubModal.tsx | 5 +- .../__tests__/topology-knative-test-data.ts | 54 +- .../__tests__/get-knative-resources.spec.ts | 10 +- .../src/utils/get-knative-resources.ts | 27 +- .../src/utils/traffic-splitting-utils.ts | 6 +- .../BareMetalHostDetailsPage.tsx | 4 +- .../baremetal-hosts/BareMetalHostsPage.tsx | 26 +- .../src/components/clusterserviceversion.tsx | 8 +- .../src/components/k8s-resource.tsx | 10 +- .../src/components/operand/index.tsx | 2 +- .../src/__tests__/topology-test-data.ts | 4 +- .../__tests__/storage-class-form.spec.tsx | 5 - frontend/public/components/cron-job.tsx | 3 + .../cluster-dashboard/health-item.tsx | 3 +- .../cluster-dashboard/inventory-card.tsx | 4 +- .../cluster-dashboard/utils.ts | 8 +- .../project-dashboard/inventory-card.tsx | 15 +- .../factory/__tests__/list-page.spec.tsx | 12 +- .../public/components/factory/details.tsx | 9 +- .../public/components/factory/list-page.tsx | 84 +- .../sidebars/resource-sidebar-samples.tsx | 4 +- .../utils/__tests__/firehose.data.tsx | 41 - .../utils/__tests__/firehose.spec.tsx | 1601 ----------------- frontend/public/components/utils/firehose.jsx | 312 ---- frontend/public/components/utils/headings.tsx | 4 +- frontend/public/components/utils/index.tsx | 1 - .../public/components/utils/list-dropdown.tsx | 5 +- frontend/public/components/utils/types.ts | 44 +- frontend/public/locales/en/public.json | 31 +- 56 files changed, 345 insertions(+), 2326 deletions(-) delete mode 100644 frontend/public/components/utils/__tests__/firehose.data.tsx delete mode 100644 frontend/public/components/utils/__tests__/firehose.spec.tsx delete mode 100644 frontend/public/components/utils/firehose.jsx diff --git a/frontend/packages/console-dynamic-plugin-sdk/docs/console-extensions.md b/frontend/packages/console-dynamic-plugin-sdk/docs/console-extensions.md index 489fce50644..f0a8752ffd0 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/docs/console-extensions.md +++ b/frontend/packages/console-dynamic-plugin-sdk/docs/console-extensions.md @@ -389,7 +389,7 @@ Adds an activity to the Activity Card of Overview Dashboard where the triggering | Name | Value Type | Optional | Description | | ---- | ---------- | -------- | ----------- | -| `k8sResource` | `CodeRef` | no | The utilization item to be replaced. | +| `k8sResource` | `CodeRef` | no | The utilization item to be replaced. | | `component` | `CodeRef>>` | no | The action component. | | `isActivity` | `CodeRef<(resource: T) => boolean>` | yes | Function which determines if the given resource represents the action. If not defined, every resource represents activity. | | `getTimestamp` | `CodeRef<(resource: T) => Date>` | yes | Timestamp for the given action, which will be used for ordering. | @@ -407,7 +407,7 @@ Adds a health subsystem to the status card of Overview dashboard where the sourc | Name | Value Type | Optional | Description | | ---- | ---------- | -------- | ----------- | | `title` | `string` | no | Title of operators section in the popup. | -| `resources` | `CodeRef` | no | Kubernetes resources which will be fetched and passed to `healthHandler`. | +| `resources` | `CodeRef` | no | Kubernetes resources which will be fetched and passed to `healthHandler`. | | `getOperatorsWithStatuses` | `CodeRef>` | yes | Resolves status for the operators. | | `operatorRowLoader` | `CodeRef>>` | yes | Loader for popup row component. | | `viewAllLink` | `string` | yes | Links to all resources page. If not provided then a list page of the first resource from resources prop is used. | @@ -427,7 +427,7 @@ Adds a health subsystem to the status card of Overview dashboard where the sourc | `title` | `string` | no | The display name of the subsystem. | | `queries` | `string[]` | no | The Prometheus queries | | `healthHandler` | `CodeRef` | no | Resolve the subsystem's health. | -| `additionalResource` | `CodeRef` | yes | Additional resource which will be fetched and passed to `healthHandler`. | +| `additionalResource` | `CodeRef` | yes | Additional resource which will be fetched and passed to `healthHandler`. | | `popupComponent` | `CodeRef>` | yes | Loader for popup content. If defined, a health item will be represented as a link which opens popup with given content. | | `popupTitle` | `string` | yes | The title of the popover. | | `popupClassname` | `string` | yes | Optional classname for the popup top-level component. | @@ -468,8 +468,8 @@ Adds a health subsystem to the status card of Overview dashboard where the sourc | `url` | `string` | no | The URL to fetch data from. It will be prefixed with base k8s URL. | | `healthHandler` | `CodeRef>` | no | Resolve the subsystem's health. | | `fetch` | `CodeRef` | yes | Custom function to fetch data from the URL.
If none is specified, default one (`coFetchJson`) will be used.
Response is then parsed by `healthHandler`. | -| `additionalResource` | `CodeRef` | yes | Additional resource which will be fetched and passed to `healthHandler`. | -| `popupComponent` | `CodeRef; }>>` | yes | Loader for popup content. If defined, a health item will be represented as a link which opens popup with given content. | +| `additionalResource` | `CodeRef` | yes | Additional resource which will be fetched and passed to `healthHandler`. | +| `popupComponent` | `CodeRef; }>>` | yes | Loader for popup content. If defined, a health item will be represented as a link which opens popup with given content. | | `popupTitle` | `string` | yes | The title of the popover. | --- diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-types.ts index 8740bbf3f18..1bcbfe9bf22 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-types.ts @@ -78,7 +78,7 @@ export type HealthItemProps = WithClassNameProps<{ export type ResourceInventoryItemProps = { resources: K8sResourceCommon[]; - additionalResources?: { [key: string]: [] }; + additionalResources?: { [key: string]: K8sResourceCommon[] }; mapper?: StatusGroupMapper; kind: K8sModel; isLoading: boolean; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts index a1783812eeb..b2e58358e08 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts @@ -226,31 +226,12 @@ export type WatchK8sResourcesGeneric = { }; }; -export type FirehoseResource = { - kind: K8sResourceKindReference; - name?: string; - namespace?: string; - isList?: boolean; - selector?: Selector; +/** + * Extension of WatchK8sResource that includes the prop field required by + * components and extension types. + */ +export type WatchK8sResourceWithProp = WatchK8sResource & { prop: string; - namespaced?: boolean; - optional?: boolean; - limit?: number; - fieldSelector?: string; -}; - -export type FirehoseResult< - R extends K8sResourceCommon | K8sResourceCommon[] = K8sResourceCommon[] -> = { - loaded: boolean; - loadError: string; - optional?: boolean; - data: R; - kind?: string; -}; - -export type FirehoseResourcesResult = { - [key: string]: FirehoseResult; }; export type WatchK8sResult = [R, boolean, any]; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/dashboard-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/dashboard-types.ts index 229a890afd4..bb217d5b0b9 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/dashboard-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/dashboard-types.ts @@ -6,8 +6,7 @@ import type { PrometheusResponse, ResourcesObject, WatchK8sResults, - FirehoseResourcesResult, - FirehoseResult, + WatchK8sResultsObject, OverviewCardSpan, K8sResourceKind, } from './console-types'; @@ -18,7 +17,7 @@ import type { export type CardSpan = OverviewCardSpan; export type GetOperatorsWithStatuses = ( - resources: FirehoseResourcesResult, + resources: WatchK8sResults, ) => OperatorStatusWithResources[]; export type K8sActivityProps = { @@ -53,13 +52,13 @@ export type OperatorHealth = { export type PrometheusHealthHandler = ( responses: { response: PrometheusResponse; error: any }[], t?: TFunction, - additionalResource?: FirehoseResult, + additionalResource?: WatchK8sResultsObject, infrastructure?: K8sResourceKind, ) => SubsystemHealth; export type PrometheusHealthPopupProps = { responses: { response: PrometheusResponse; error: any }[]; - k8sResult?: FirehoseResult; + k8sResult?: WatchK8sResultsObject; hide: () => void; }; @@ -80,7 +79,7 @@ export type SubsystemHealth = { export type URLHealthHandler< R, T extends K8sResourceCommon | K8sResourceCommon[] = K8sResourceCommon | K8sResourceCommon[] -> = (response: R, error: any, additionalResource?: FirehoseResult) => SubsystemHealth; +> = (response: R, error: any, additionalResource?: WatchK8sResultsObject) => SubsystemHealth; export type StatusPopupItemProps = { children: ReactNode; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/dashboards.ts b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/dashboards.ts index e80f6e60cc5..db714822ee0 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/extensions/dashboards.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/extensions/dashboards.ts @@ -8,8 +8,8 @@ import type { StatusGroupMapper, WatchK8sResources, WatchK8sResults, - FirehoseResource, - FirehoseResult, + WatchK8sResourceWithProp, + WatchK8sResultsObject, } from './console-types'; import type { CardSpan, @@ -62,7 +62,7 @@ export type DashboardsOverviewHealthPrometheusSubsystem = Extension< /** Resolve the subsystem's health. */ healthHandler: CodeRef; /** Additional resource which will be fetched and passed to `healthHandler`. */ - additionalResource?: CodeRef; + additionalResource?: CodeRef; /** Loader for popup content. If defined, a health item will be represented as a link which opens popup with given content. */ popupComponent?: CodeRef>; /** The title of the popover. */ @@ -96,13 +96,13 @@ export type DashboardsOverviewHealthURLSubsystem< */ fetch?: CodeRef; /** Additional resource which will be fetched and passed to `healthHandler`. */ - additionalResource?: CodeRef; + additionalResource?: CodeRef; /** Loader for popup content. If defined, a health item will be represented as a link which opens popup with given content. */ popupComponent?: CodeRef< React.ComponentType<{ healthResult?: T; healthResultError?: any; - k8sResult?: FirehoseResult; + k8sResult?: WatchK8sResultsObject; }> >; /** The title of the popover. */ @@ -138,7 +138,7 @@ export type DashboardsOverviewHealthOperator< /** Title of operators section in the popup. */ title: string; /** Kubernetes resources which will be fetched and passed to `healthHandler`. */ - resources: CodeRef; + resources: CodeRef; /** Resolves status for the operators. */ getOperatorsWithStatuses?: CodeRef>; /** Loader for popup row component. */ @@ -193,7 +193,7 @@ export type DashboardsOverviewResourceActivity< 'console.dashboards/overview/activity/resource', { /** The utilization item to be replaced. */ - k8sResource: CodeRef; + k8sResource: CodeRef; /** Function which determines if the given resource represents the action. If not defined, every resource represents activity. */ isActivity?: CodeRef<(resource: T) => boolean>; /** Timestamp for the given action, which will be used for ordering. */ diff --git a/frontend/packages/console-shared/src/components/dashboard/status-card/OperatorStatusBody.tsx b/frontend/packages/console-shared/src/components/dashboard/status-card/OperatorStatusBody.tsx index 80797a146fc..ab02edd71b2 100644 --- a/frontend/packages/console-shared/src/components/dashboard/status-card/OperatorStatusBody.tsx +++ b/frontend/packages/console-shared/src/components/dashboard/status-card/OperatorStatusBody.tsx @@ -3,9 +3,13 @@ import { useCallback } from 'react'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom-v5-compat'; -import type { GetOperatorsWithStatuses, OperatorRowProps } from '@console/dynamic-plugin-sdk'; +import type { + GetOperatorsWithStatuses, + OperatorRowProps, + WatchK8sResults, + K8sResourceCommon, +} from '@console/dynamic-plugin-sdk'; import type { LazyLoader } from '@console/internal/components/utils/async'; -import type { FirehoseResourcesResult } from '@console/internal/components/utils/types'; import { getMostImportantStatuses } from './state-utils'; import { HealthState } from './states'; import StatusItem, { StatusPopupSection } from './StatusPopup'; @@ -75,7 +79,7 @@ export const OperatorsSection: FC = ({ }; type OperatorsSectionProps = { - resources: FirehoseResourcesResult; + resources: WatchK8sResults<{ [key: string]: K8sResourceCommon | K8sResourceCommon[] }>; getOperatorsWithStatuses: GetOperatorsWithStatuses; title: string; linkTo: string; diff --git a/frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx b/frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx index 52312388707..27cf5c722ae 100644 --- a/frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx +++ b/frontend/packages/console-shared/src/components/dropdown/ResourceDropdown.tsx @@ -4,11 +4,11 @@ import * as fuzzy from 'fuzzysearch'; import type { TFunction } from 'i18next'; import * as _ from 'lodash'; import { withTranslation } from 'react-i18next'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import type { ConsoleSelectProps } from '@console/internal/components/utils/console-select'; import { ConsoleSelect } from '@console/internal/components/utils/console-select'; import { ResourceIcon } from '@console/internal/components/utils/resource-icon'; import { LoadingInline } from '@console/internal/components/utils/status-box'; -import type { FirehoseResult } from '@console/internal/components/utils/types'; import type { K8sResourceKind, K8sKind } from '@console/internal/module/k8s'; import { referenceForModel, modelFor, referenceFor } from '@console/internal/module/k8s'; @@ -65,7 +65,7 @@ export interface ResourceDropdownProps { transformLabel?: Function; loaded?: boolean; loadError?: string; - resources?: FirehoseResult[]; + resources?: WatchK8sResultsObject[]; autoSelect?: boolean; resourceFilter?: (resource: K8sResourceKind) => boolean; onChange?: (key: string, name?: string | object, selectedResource?: K8sResourceKind) => void; @@ -195,15 +195,16 @@ class ResourceDropdownInternal extends Component { + _.each(resources, ({ data }) => { + const dataArray = Array.isArray(data) ? data : [data]; _.reduce( - data, + dataArray, (acc, resource) => { const { customKey, key: name } = this.craftResourceKey(resource, props); const dataValue = customKey || name; if (dataValue) { if (showBadge) { - const model = modelFor(referenceFor(resource)) || (kind && modelFor(kind)); + const model = modelFor(referenceFor(resource)); acc[dataValue] = model ? ( ) : ( diff --git a/frontend/packages/console-shared/src/components/formik-fields/ResourceDropdownField.tsx b/frontend/packages/console-shared/src/components/formik-fields/ResourceDropdownField.tsx index 9d1243ddc99..2c0663acc87 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/ResourceDropdownField.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/ResourceDropdownField.tsx @@ -1,8 +1,9 @@ import type { FC } from 'react'; +import { useMemo } from 'react'; import { FormGroup, FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core'; import type { FormikValues } from 'formik'; import { useField, useFormikContext } from 'formik'; -import type { FirehoseResult } from '@console/internal/components/utils/types'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import type { K8sResourceKind } from '@console/internal/module/k8s'; import { useFormikValidationFix } from '../../hooks/formik-validation-fix'; import type { ResourceDropdownItems } from '../dropdown/ResourceDropdown'; @@ -12,7 +13,7 @@ import { getFieldId } from './field-utils'; export interface ResourceDropdownFieldProps extends DropdownFieldProps { dataSelector: string[] | number[] | symbol[]; - resources: FirehoseResult[]; + resources: WatchK8sResultsObject[]; showBadge?: boolean; onLoad?: (items: ResourceDropdownItems) => void; onChange?: (key: string, name?: string | object, resource?: K8sResourceKind) => void; @@ -49,6 +50,19 @@ const ResourceDropdownField: FC = ({ useFormikValidationFix(field.value); + // ResourceDropdown expects these as top-level props to manage loading state + const { loaded, loadError } = useMemo(() => { + if (!resources) { + return { loaded: true, loadError: undefined }; + } + const allLoaded = resources.every((r) => r.loaded); + const resourceWithLoadError = resources.find((r) => r.loadError); + return { + loaded: allLoaded, + loadError: resourceWithLoadError?.loadError, + }; + }, [resources]); + return ( = ({ onLoad={onLoad} resourceFilter={resourceFilter} resources={resources} + loaded={loaded} + loadError={loadError} onChange={(value: string, name: string | object, resource: K8sResourceKind) => { props.onChange && props.onChange(value, name, resource); setFieldValue(props.name, value); diff --git a/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts b/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts index 98ec99aa5c7..1c160e09e21 100644 --- a/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts +++ b/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts @@ -3,9 +3,13 @@ import YAML from 'js-yaml'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { PodDisruptionBudgetModel } from '@console/app/src/models'; -import type { AddAction, CatalogItemType, Perspective } from '@console/dynamic-plugin-sdk'; +import type { + AddAction, + CatalogItemType, + Perspective, + WatchK8sResultsObject, +} from '@console/dynamic-plugin-sdk'; import { isAddAction, isCatalogItemType, isPerspective } from '@console/dynamic-plugin-sdk'; -import type { FirehoseResult } from '@console/internal/components/utils/types'; import { BuildConfigModel, ClusterRoleModel, @@ -331,7 +335,10 @@ const useDefaultSamples = () => { ); }; -export const useResourceSidebarSamples = (kindObj: K8sKind, yamlSamplesList: FirehoseResult) => { +export const useResourceSidebarSamples = ( + kindObj: K8sKind, + yamlSamplesList: WatchK8sResultsObject, +) => { const defaultSamples = useDefaultSamples(); if (!kindObj) { diff --git a/frontend/packages/console-shared/src/types/pod.ts b/frontend/packages/console-shared/src/types/pod.ts index c79d891d1aa..416a15587fc 100644 --- a/frontend/packages/console-shared/src/types/pod.ts +++ b/frontend/packages/console-shared/src/types/pod.ts @@ -1,8 +1,9 @@ import type { ExtPodKind, + K8sResourceCommon, PodControllerOverviewItem, + WatchK8sResultsObject, } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; -import type { FirehoseResult } from '@console/internal/components/utils/types'; import type { DeploymentKind, PodKind } from '@console/internal/module/k8s'; export type { @@ -15,19 +16,19 @@ export type { } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; export interface PodDataResources { - replicationControllers: FirehoseResult; - replicaSets: FirehoseResult; - pods: FirehoseResult; - deploymentConfigs?: FirehoseResult; - deployments?: FirehoseResult; + replicationControllers: WatchK8sResultsObject; + replicaSets: WatchK8sResultsObject; + pods: WatchK8sResultsObject; + deploymentConfigs?: WatchK8sResultsObject; + deployments?: WatchK8sResultsObject; } export interface PodRingResources { - pods: FirehoseResult; - replicaSets: FirehoseResult; - replicationControllers: FirehoseResult; - deployments?: FirehoseResult; - deploymentConfigs?: FirehoseResult; + pods: WatchK8sResultsObject; + replicaSets: WatchK8sResultsObject; + replicationControllers: WatchK8sResultsObject; + deployments?: WatchK8sResultsObject; + deploymentConfigs?: WatchK8sResultsObject; } export interface PodRingData { diff --git a/frontend/packages/console-shared/src/utils/__tests__/test-resource-data.ts b/frontend/packages/console-shared/src/utils/__tests__/test-resource-data.ts index dcfaaa08a24..715d3d08c27 100644 --- a/frontend/packages/console-shared/src/utils/__tests__/test-resource-data.ts +++ b/frontend/packages/console-shared/src/utils/__tests__/test-resource-data.ts @@ -1,8 +1,8 @@ -import type { FirehoseResult } from '@console/internal/components/utils'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import type { DeploymentKind, PodKind, K8sResourceKind } from '@console/internal/module/k8s'; import { ImagePullPolicy } from '@console/internal/module/k8s'; -export const sampleDeploymentConfigs: FirehoseResult = { +export const sampleDeploymentConfigs: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -175,7 +175,7 @@ export const sampleDeploymentConfigs: FirehoseResult = { }, ], }; -export const sampleDeployments: FirehoseResult = { +export const sampleDeployments: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -450,7 +450,7 @@ export const sampleDeployments: FirehoseResult = { ], }; -export const samplePods: FirehoseResult = { +export const samplePods: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1258,7 +1258,7 @@ export const samplePods: FirehoseResult = { ], }; -export const sampleReplicationControllers: FirehoseResult = { +export const sampleReplicationControllers: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1304,7 +1304,7 @@ export const sampleReplicationControllers: FirehoseResult = { ], }; -export const sampleReplicaSets: FirehoseResult = { +export const sampleReplicaSets: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1394,7 +1394,7 @@ export const sampleReplicaSets: FirehoseResult = { ], }; -export const sampleServices: FirehoseResult = { +export const sampleServices: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1511,7 +1511,7 @@ export const sampleServices: FirehoseResult = { }, ], }; -export const sampleRoutes: FirehoseResult = { +export const sampleRoutes: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1629,7 +1629,7 @@ export const sampleRoutes: FirehoseResult = { ], }; -export const sampleBuildConfigs: FirehoseResult = { +export const sampleBuildConfigs: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1822,7 +1822,7 @@ export const sampleBuildConfigs: FirehoseResult = { ], }; -export const sampleBuilds: FirehoseResult = { +export const sampleBuilds: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1919,7 +1919,7 @@ export const sampleBuilds: FirehoseResult = { ], }; -export const sampleDaemonSets: FirehoseResult = { +export const sampleDaemonSets: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1995,7 +1995,7 @@ export const sampleDaemonSets: FirehoseResult = { ], }; -export const sampleStatefulSets: FirehoseResult = { +export const sampleStatefulSets: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -2139,7 +2139,7 @@ export const sampleStatefulSets: FirehoseResult = { ], }; -export const sampleJobs: FirehoseResult = { +export const sampleJobs: WatchK8sResultsObject = { data: [ { kind: 'Job', @@ -2623,7 +2623,7 @@ export const sampleJobs: FirehoseResult = { loadError: '', }; -export const sampleCronJobs: FirehoseResult = { +export const sampleCronJobs: WatchK8sResultsObject = { data: [ { kind: 'CronJob', @@ -2800,7 +2800,7 @@ export const sampleCronJobs: FirehoseResult = { loadError: '', }; -export const samplePipeline: FirehoseResult = { +export const samplePipeline: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -2832,7 +2832,7 @@ export const samplePipeline: FirehoseResult = { ], }; -export const samplePipelineRun: FirehoseResult = { +export const samplePipelineRun: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -2909,7 +2909,7 @@ export const samplePipelineRun: FirehoseResult = { ], }; -export const sampleClusterServiceVersions: FirehoseResult = { +export const sampleClusterServiceVersions: WatchK8sResultsObject = { data: [ { apiVersion: 'operators.coreos.com/v1alpha1', diff --git a/frontend/packages/console-shared/src/utils/utils.ts b/frontend/packages/console-shared/src/utils/utils.ts index 3e718ccfed2..e5a933bd096 100644 --- a/frontend/packages/console-shared/src/utils/utils.ts +++ b/frontend/packages/console-shared/src/utils/utils.ts @@ -1,7 +1,7 @@ import i18next from 'i18next'; import type { JSONSchema7 } from 'json-schema'; import { startCase, toPath } from 'lodash'; -import type { FirehoseResult } from '@console/internal/components/utils/types'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import type { K8sKind, K8sResourceKind } from '@console/internal/module/k8s'; import { modelFor } from '@console/internal/module/k8s'; import { getUID } from '../selectors/common'; @@ -22,7 +22,7 @@ export const createBasicLookup = (list: A[], getKey: KeyResolver): EntityM }; export const createLookup = ( - loadingList: FirehoseResult, + loadingList: WatchK8sResultsObject, getKey?: KeyResolver, ): K8sEntityMap => { if (loadingList && loadingList.loaded) { diff --git a/frontend/packages/container-security/src/components/ImageVulnerabilitiesList.tsx b/frontend/packages/container-security/src/components/ImageVulnerabilitiesList.tsx index cb45c962fda..bf3aa41724a 100644 --- a/frontend/packages/container-security/src/components/ImageVulnerabilitiesList.tsx +++ b/frontend/packages/container-security/src/components/ImageVulnerabilitiesList.tsx @@ -2,9 +2,8 @@ import type { FC } from 'react'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom-v5-compat'; -import type { RowFilter } from '@console/dynamic-plugin-sdk'; +import type { RowFilter, WatchK8sResults } from '@console/dynamic-plugin-sdk'; import { MultiListPage } from '@console/internal/components/factory'; -import type { FirehoseResourcesResult } from '@console/internal/components/utils'; import { referenceForModel } from '@console/internal/module/k8s'; import { Priority, priorityFor } from '../const'; import { ImageManifestVulnModel } from '../models'; @@ -76,9 +75,7 @@ const ImageVulnerabilitiesList: FC = (props) => { }, ]} title={t('container-security~Vulnerabilities')} - flatten={( - resources: FirehoseResourcesResult<{ imageVulnerabilities: ImageManifestVuln }>, - ) => { + flatten={(resources: WatchK8sResults<{ imageVulnerabilities: ImageManifestVuln }>) => { return _.sortBy( _.flatten( (resources?.imageVulnerabilities?.data?.spec?.features ?? []).map((feature) => diff --git a/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/SecretsSection.spec.tsx b/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/SecretsSection.spec.tsx index cf045280371..df0d8aec088 100644 --- a/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/SecretsSection.spec.tsx +++ b/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/SecretsSection.spec.tsx @@ -8,12 +8,6 @@ import store from '@console/internal/redux'; import type { SecretsSectionFormData } from '../SecretsSection'; import SecretsSection from '../SecretsSection'; -// Skip Firehose fetching and render just the children -jest.mock('@console/internal/components/utils/firehose', () => ({ - ...jest.requireActual('@console/internal/components/utils/firehose'), - Firehose: ({ children }) => children, -})); - interface WrapperProps extends FormikConfig { children?: ReactNode; } diff --git a/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/SourceSection.spec.tsx b/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/SourceSection.spec.tsx index 2a03b3ae268..3dc06f8839c 100644 --- a/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/SourceSection.spec.tsx +++ b/frontend/packages/dev-console/src/components/buildconfig/sections/__tests__/SourceSection.spec.tsx @@ -12,12 +12,6 @@ import { BuildStrategyType } from '../../types'; import type { SourceSectionFormData } from '../SourceSection'; import SourceSection from '../SourceSection'; -// Skip Firehose fetching and render just the children -jest.mock('@console/internal/components/utils/firehose', () => ({ - ...jest.requireActual('@console/internal/components/utils/firehose'), - Firehose: ({ children }) => children, -})); - // Skip network calls to any external git service jest.mock('@console/git-service', () => ({ ...jest.requireActual('@console/git-service'), diff --git a/frontend/packages/dev-console/src/components/catalog/providers/useTemplates.tsx b/frontend/packages/dev-console/src/components/catalog/providers/useTemplates.tsx index 6a6cb5743fe..602cd5774e6 100644 --- a/frontend/packages/dev-console/src/components/catalog/providers/useTemplates.tsx +++ b/frontend/packages/dev-console/src/components/catalog/providers/useTemplates.tsx @@ -79,9 +79,8 @@ const useTemplates: ExtensionHook[]> = ({ const [projectTemplatesLoaded, setProjectTemplatesLoaded] = useState(false); const [projectTemplatesError, setProjectTemplatesError] = useState(); - // Load templates from the shared `openshift` namespace. Don't use Firehose - // for templates so that we can request only metadata. This keeps the request - // much smaller. + // Load templates from the shared `openshift` namespace. + // Request only metadata to keep the request much smaller. useEffect(() => { k8sListPartialMetadata(TemplateModel, { ns: 'openshift' }) .then((metadata) => { diff --git a/frontend/packages/dev-console/src/components/dropdown/SourceSecretDropdown.tsx b/frontend/packages/dev-console/src/components/dropdown/SourceSecretDropdown.tsx index 83ec899c14b..4c8358e4417 100644 --- a/frontend/packages/dev-console/src/components/dropdown/SourceSecretDropdown.tsx +++ b/frontend/packages/dev-console/src/components/dropdown/SourceSecretDropdown.tsx @@ -1,7 +1,9 @@ import type { FC } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Firehose } from '@console/internal/components/utils/firehose'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { SecretModel } from '@console/internal/models'; +import type { SecretKind } from '@console/internal/module/k8s'; import type { ResourceDropdownProps } from '@console/shared/src/components/dropdown/ResourceDropdown'; import { ResourceDropdown } from '@console/shared/src/components/dropdown/ResourceDropdown'; @@ -12,26 +14,48 @@ interface SourceSecretDropdownProps const SourceSecretDropdown: FC = (props) => { const { t } = useTranslation(); - const filterData = (item) => { + const filterData = (item: SecretKind) => { return item.type === 'kubernetes.io/basic-auth' || item.type === 'kubernetes.io/ssh-auth'; }; - const resources = [ - { - isList: true, - namespace: props.namespace, - kind: SecretModel.kind, - prop: 'secrets', - }, - ]; + + const watchSpec = useMemo( + () => ({ + secrets: { + isList: true, + namespace: props.namespace, + kind: SecretModel.kind, + optional: true, + }, + }), + [props.namespace], + ); + + const watchedResources = useK8sWatchResources<{ secrets: SecretKind[] }>(watchSpec); + + const resources = useMemo( + () => [ + { + data: watchedResources.secrets?.data, + loaded: watchedResources.secrets?.loaded, + loadError: watchedResources.secrets?.loadError, + kind: SecretModel.kind, + }, + ], + [ + watchedResources.secrets?.data, + watchedResources.secrets?.loaded, + watchedResources.secrets?.loadError, + ], + ); + return ( - - - + ); }; diff --git a/frontend/packages/dev-console/src/components/edit-application/edit-application-types.ts b/frontend/packages/dev-console/src/components/edit-application/edit-application-types.ts index 202fbe164a2..25467571c4d 100644 --- a/frontend/packages/dev-console/src/components/edit-application/edit-application-types.ts +++ b/frontend/packages/dev-console/src/components/edit-application/edit-application-types.ts @@ -1,16 +1,16 @@ -import type { FirehoseResult } from '@console/internal/components/utils'; +import type { K8sResourceCommon, WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import type { K8sResourceKind } from '@console/internal/module/k8s'; import type { PipelineKind } from '../../types/pipeline'; export interface AppResources { - service?: FirehoseResult; - route?: FirehoseResult; - buildConfig?: FirehoseResult; - shipwrightBuild?: FirehoseResult; - pipeline?: FirehoseResult; - imageStream?: FirehoseResult; - editAppResource?: FirehoseResult; - imageStreams?: FirehoseResult; + service?: WatchK8sResultsObject; + route?: WatchK8sResultsObject; + buildConfig?: WatchK8sResultsObject; + shipwrightBuild?: WatchK8sResultsObject; + pipeline?: WatchK8sResultsObject; + imageStream?: WatchK8sResultsObject; + editAppResource?: WatchK8sResultsObject; + imageStreams?: WatchK8sResultsObject; } export interface EditApplicationProps { diff --git a/frontend/packages/dev-console/src/components/health-checks/AddHealthChecksForm.tsx b/frontend/packages/dev-console/src/components/health-checks/AddHealthChecksForm.tsx index 7bf2a105f2f..d453b08ae0f 100644 --- a/frontend/packages/dev-console/src/components/health-checks/AddHealthChecksForm.tsx +++ b/frontend/packages/dev-console/src/components/health-checks/AddHealthChecksForm.tsx @@ -5,7 +5,7 @@ import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom-v5-compat'; import * as yup from 'yup'; -import type { FirehoseResult } from '@console/internal/components/utils'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import { LoadingBox, StatusBox } from '@console/internal/components/utils'; import type { K8sResourceKind } from '@console/internal/module/k8s'; import { k8sUpdate, modelFor, referenceFor } from '@console/internal/module/k8s'; @@ -16,7 +16,7 @@ import { healthChecksProbesValidationSchema } from './health-checks-probe-valida import { updateHealthChecksProbe } from './health-checks-utils'; type AddHealthChecksFormProps = { - resource?: FirehoseResult; + resource?: WatchK8sResultsObject; currentContainer: string; }; diff --git a/frontend/packages/dev-console/src/utils/imagestream-utils.ts b/frontend/packages/dev-console/src/utils/imagestream-utils.ts index 760d0fb24a9..d0d90332c17 100644 --- a/frontend/packages/dev-console/src/utils/imagestream-utils.ts +++ b/frontend/packages/dev-console/src/utils/imagestream-utils.ts @@ -10,7 +10,7 @@ import { getMostRecentBuilderTag, getBuilderTagsSortedByVersion, } from '@console/internal/components/image-stream'; -import type { FirehoseResource } from '@console/internal/components/utils'; +import type { WatchK8sResourceWithProp } from '@console/internal/components/utils/types'; import { ProjectModel, ImageStreamModel } from '@console/internal/models'; import type { ContainerPort, @@ -190,7 +190,7 @@ export const getImageStreamTags = (imageStream: K8sResourceKind) => { }, {}); }; -export const getProjectResource = (): FirehoseResource[] => { +export const getProjectResource = (): WatchK8sResourceWithProp[] => { return [ { isList: true, @@ -200,7 +200,7 @@ export const getProjectResource = (): FirehoseResource[] => { ]; }; -export const getImageStreamResource = (namespace: string): FirehoseResource[] => { +export const getImageStreamResource = (namespace: string): WatchK8sResourceWithProp[] => { const resource = []; if (namespace) { resource.push({ diff --git a/frontend/packages/helm-plugin/src/components/__tests__/helm-release-mock-data.ts b/frontend/packages/helm-plugin/src/components/__tests__/helm-release-mock-data.ts index e1696d2b666..bd97a62af82 100644 --- a/frontend/packages/helm-plugin/src/components/__tests__/helm-release-mock-data.ts +++ b/frontend/packages/helm-plugin/src/components/__tests__/helm-release-mock-data.ts @@ -1,5 +1,5 @@ -import type { FirehoseResourcesResult } from '@console/internal/components/utils/types'; -import type { K8sResourceCommon, K8sResourceKind } from '@console/internal/module/k8s'; +import type { K8sResourceCommon, WatchK8sResults } from '@console/dynamic-plugin-sdk'; +import type { K8sResourceKind } from '@console/internal/module/k8s'; import type { HelmRelease, HelmChartMetaData, HelmChartEntries } from '../../types/helm-types'; /* eslint-disable @typescript-eslint/naming-convention */ @@ -260,7 +260,7 @@ export const mockChartEntries1: HelmChartEntries = { 'rh-hazelcast-enterprise--redhat-helm-repo': mockRedhatHelmChartData, }; -export const mockReleaseResources: FirehoseResourcesResult<{ +export const mockReleaseResources: WatchK8sResults<{ Deployment: K8sResourceCommon; StatefulSet: K8sResourceCommon; Pod: K8sResourceCommon; diff --git a/frontend/packages/helm-plugin/src/components/details-page/HelmReleaseDetails.tsx b/frontend/packages/helm-plugin/src/components/details-page/HelmReleaseDetails.tsx index 3b1b4018aa6..a59f992c580 100755 --- a/frontend/packages/helm-plugin/src/components/details-page/HelmReleaseDetails.tsx +++ b/frontend/packages/helm-plugin/src/components/details-page/HelmReleaseDetails.tsx @@ -4,9 +4,9 @@ import { Badge } from '@patternfly/react-core'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { useParams, useLocation } from 'react-router-dom-v5-compat'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import { ErrorPage404 } from '@console/internal/components/error'; import { DetailsPage } from '@console/internal/components/factory'; -import type { FirehoseResult } from '@console/internal/components/utils'; import { navFactory, LoadingBox, StatusBox } from '@console/internal/components/utils'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { SecretModel } from '@console/internal/models'; @@ -23,7 +23,7 @@ import HelmReleaseResources from './resources/HelmReleaseResources'; const SecretReference: K8sResourceKindReference = 'Secret'; const HelmReleaseReference = 'HelmRelease'; interface HelmReleaseDetailsProps { - secrets?: FirehoseResult; + secrets?: WatchK8sResultsObject; } interface LoadedHelmReleaseDetailsProps extends HelmReleaseDetailsProps { @@ -202,13 +202,13 @@ const HelmReleaseDetails: FC = () => { data: helmReleaseData, }; - const secretsFirehoseResult: FirehoseResult = { + const secretsResult: WatchK8sResultsObject = { loaded: secretLoaded, loadError: secretLoadError, data: secrets, }; - return ; + return ; }; export default HelmReleaseDetails; diff --git a/frontend/packages/helm-plugin/src/components/details-page/__tests__/HelmReleaseDetails.spec.tsx b/frontend/packages/helm-plugin/src/components/details-page/__tests__/HelmReleaseDetails.spec.tsx index 29b0ff6fdfb..19f429fd8eb 100644 --- a/frontend/packages/helm-plugin/src/components/details-page/__tests__/HelmReleaseDetails.spec.tsx +++ b/frontend/packages/helm-plugin/src/components/details-page/__tests__/HelmReleaseDetails.spec.tsx @@ -22,11 +22,6 @@ jest.mock('@console/shared/src/hooks/version', () => ({ useClusterVersion: jest.fn(), })); -jest.mock('@console/internal/components/utils/firehose', () => ({ - ...jest.requireActual('@console/internal/components/utils/firehose'), - Firehose: ({ children }) => children, -})); - describe('HelmReleaseDetails', () => { beforeEach(() => { helmReleaseDetailsProps = { diff --git a/frontend/packages/helm-plugin/src/components/details-page/resources/HelmReleaseResources.tsx b/frontend/packages/helm-plugin/src/components/details-page/resources/HelmReleaseResources.tsx index ea852445348..104bcd62726 100644 --- a/frontend/packages/helm-plugin/src/components/details-page/resources/HelmReleaseResources.tsx +++ b/frontend/packages/helm-plugin/src/components/details-page/resources/HelmReleaseResources.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom-v5-compat'; import { MultiListPage } from '@console/internal/components/factory'; -import type { FirehoseResource } from '@console/internal/components/utils'; +import type { WatchK8sResourceWithProp } from '@console/internal/components/utils/types'; import type { K8sResourceKind } from '@console/internal/module/k8s'; import { referenceFor, modelFor, referenceForModel } from '@console/internal/module/k8s'; import type { HelmRelease } from '../../../types/helm-types'; @@ -18,7 +18,7 @@ const HelmReleaseResources: FC = ({ customData }) => const params = useParams(); const namespace = params.ns; const helmManifestResources = loadHelmManifestResources(customData); - const firehoseResources: FirehoseResource[] = helmManifestResources.map( + const watchResources: WatchK8sResourceWithProp[] = helmManifestResources.map( (resource: K8sResourceKind) => { const resourceKind = referenceFor(resource); const model = modelFor(resourceKind); @@ -34,7 +34,7 @@ const HelmReleaseResources: FC = ({ customData }) => ); return ( { // mockHelmReleases[0] has an empty manifest, so no resources to watch renderWithProviders(); - // Verify useK8sWatchResources hook was called (confirms migration from Firehose to hooks) + // Verify useK8sWatchResources hook was called expect(mockUseK8sWatchResources).toHaveBeenCalled(); // Verify empty state message is displayed (user-visible content) diff --git a/frontend/packages/helm-plugin/src/topology/__tests__/helm-data-transformer.spec.ts b/frontend/packages/helm-plugin/src/topology/__tests__/helm-data-transformer.spec.ts index 379d357994c..8b96de38faf 100644 --- a/frontend/packages/helm-plugin/src/topology/__tests__/helm-data-transformer.spec.ts +++ b/frontend/packages/helm-plugin/src/topology/__tests__/helm-data-transformer.spec.ts @@ -40,11 +40,11 @@ export function getTransformedTopologyData(mockData: TopologyDataResources) { app: 'nodejs', 'app.kubernetes.io/part-of': 'app-1', }; - const fireHoseDcs = { + const modifiedDcs = { ...sampleDeploymentConfigs, data: [dc, sampleHelmChartDeploymentConfig], }; - const data = { ...mockData, deploymentConfigs: fireHoseDcs }; + const data = { ...mockData, deploymentConfigs: modifiedDcs }; const workloadResources = getWorkloadResources(data, TEST_KINDS_MAP, WORKLOAD_TYPES); const model = getHelmGraphModelFromMap(sampleHelmResourcesMap, data); diff --git a/frontend/packages/integration-tests-cypress/views/details-page.ts b/frontend/packages/integration-tests-cypress/views/details-page.ts index b5224386997..69d700ddd3b 100644 --- a/frontend/packages/integration-tests-cypress/views/details-page.ts +++ b/frontend/packages/integration-tests-cypress/views/details-page.ts @@ -13,7 +13,10 @@ export const detailsPage = { clickPageActionButton: (action: string) => { cy.byLegacyTestID('details-actions').contains(action).click(); }, - isLoaded: () => cy.byTestID('skeleton-detail-view').should('not.exist'), + isLoaded: () => { + cy.byTestID('skeleton-detail-view').should('not.exist'); + cy.get('[data-test-id="resource-title"]', { timeout: 30000 }).should('not.be.empty'); + }, breadcrumb: (breadcrumbIndex: number) => cy.byLegacyTestID(`breadcrumb-link-${breadcrumbIndex}`), selectTab: (name: string) => { cy.get(`a[data-test-id="horizontal-link-${name}"]`).should('exist').click(); diff --git a/frontend/packages/knative-plugin/src/components/sink-pubsub/SinkPubsubModal.tsx b/frontend/packages/knative-plugin/src/components/sink-pubsub/SinkPubsubModal.tsx index 3876dbf6d67..1248ee17d66 100644 --- a/frontend/packages/knative-plugin/src/components/sink-pubsub/SinkPubsubModal.tsx +++ b/frontend/packages/knative-plugin/src/components/sink-pubsub/SinkPubsubModal.tsx @@ -4,18 +4,19 @@ import type { FormikProps, FormikValues } from 'formik'; import * as fuzzy from 'fuzzysearch'; import { Trans, useTranslation } from 'react-i18next'; import FormSection from '@console/dev-console/src/components/import/section/FormSection'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import { ModalTitle, ModalBody, ModalSubmitFooter, } from '@console/internal/components/factory/modal'; -import type { FirehoseResult } from '@console/internal/components/utils/types'; +import type { K8sResourceKind } from '@console/internal/module/k8s'; import { ResourceDropdownField } from '@console/shared'; import { craftResourceKey } from '../pub-sub/pub-sub-utils'; export interface SinkPubsubModalProps { resourceName: string; - resourceDropdown: FirehoseResult[]; + resourceDropdown: WatchK8sResultsObject[]; labelTitle: string; cancel?: () => void; } diff --git a/frontend/packages/knative-plugin/src/topology/__tests__/topology-knative-test-data.ts b/frontend/packages/knative-plugin/src/topology/__tests__/topology-knative-test-data.ts index 8f7cb4afe67..4f69ebb8188 100644 --- a/frontend/packages/knative-plugin/src/topology/__tests__/topology-knative-test-data.ts +++ b/frontend/packages/knative-plugin/src/topology/__tests__/topology-knative-test-data.ts @@ -1,4 +1,4 @@ -import type { FirehoseResult } from '@console/internal/components/utils'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import type { DeploymentKind, PodKind, K8sResourceKind } from '@console/internal/module/k8s'; import { K8sResourceConditionStatus, referenceForModel } from '@console/internal/module/k8s'; import type { TopologyDataResources } from '@console/topology/src/topology-types'; @@ -41,7 +41,7 @@ import { URI_KIND } from '../const'; import type { KnativeServiceOverviewItem, KnativeTopologyDataObject } from '../topology-types'; import { NodeType } from '../topology-types'; -export const sampleDeploymentsCamelConnector: FirehoseResult = { +export const sampleDeploymentsCamelConnector: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -190,7 +190,7 @@ export const sampleDeploymentsCamelConnector: FirehoseResult = ], }; -export const sampleKnativeDeployments: FirehoseResult = { +export const sampleKnativeDeployments: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -332,7 +332,7 @@ export const sampleKnativeDeployments: FirehoseResult = { ], }; -export const sampleKnativeReplicaSets: FirehoseResult = { +export const sampleKnativeReplicaSets: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -386,43 +386,43 @@ export const sampleKnativeReplicaSets: FirehoseResult = { ], }; -export const sampleKnativePods: FirehoseResult = { +export const sampleKnativePods: WatchK8sResultsObject = { loaded: true, loadError: '', data: [], }; -export const sampleKnativeReplicationControllers: FirehoseResult = { +export const sampleKnativeReplicationControllers: WatchK8sResultsObject = { loaded: true, loadError: '', data: [], }; -export const sampleKnativeDeploymentConfigs: FirehoseResult = { +export const sampleKnativeDeploymentConfigs: WatchK8sResultsObject = { loaded: true, loadError: '', data: [], }; -export const sampleRoutes: FirehoseResult = { +export const sampleRoutes: WatchK8sResultsObject = { loaded: true, loadError: '', data: [], }; -const sampleKnativeBuildConfigs: FirehoseResult = { +const sampleKnativeBuildConfigs: WatchK8sResultsObject = { loaded: true, loadError: '', data: [], }; -const sampleKnativeBuilds: FirehoseResult = { +const sampleKnativeBuilds: WatchK8sResultsObject = { loaded: true, loadError: '', data: [], }; -export const sampleKnativeBuildConfigs2: FirehoseResult = { +export const sampleKnativeBuildConfigs2: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -521,7 +521,7 @@ export const sampleKnativeBuildConfigs2: FirehoseResult = { ], }; -export const sampleKnativeConfigurations: FirehoseResult = { +export const sampleKnativeConfigurations: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -619,7 +619,7 @@ export const revisionObj: RevisionKind = { ], }, }; -export const sampleKnativeRevisions: FirehoseResult = { +export const sampleKnativeRevisions: WatchK8sResultsObject = { loaded: true, loadError: '', data: [revisionObj], @@ -662,7 +662,7 @@ export const knativeRouteObj: RouteKind = { }, }; -export const sampleKnativeRoutes: FirehoseResult = { +export const sampleKnativeRoutes: WatchK8sResultsObject = { loaded: true, loadError: '', data: [knativeRouteObj], @@ -715,7 +715,7 @@ export const serverlessFunctionObj = { metadata: { ...knativeServiceObj.metadata, labels: { [SERVERLESS_FUNCTION_LABEL]: 'true' } }, }; -export const sampleKnativeServices: FirehoseResult = { +export const sampleKnativeServices: WatchK8sResultsObject = { loaded: true, loadError: '', data: [knativeServiceObj], @@ -725,7 +725,7 @@ export const getEventSourceResponse = ( apiGroup: string, apiVersion: string, kind: string, -): FirehoseResult => { +): WatchK8sResultsObject => { return { loaded: true, loadError: '', @@ -785,7 +785,7 @@ export const kafkaConnectionData = { ], }; -export const sampleEventSourceSinkbinding: FirehoseResult = { +export const sampleEventSourceSinkbinding: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -821,7 +821,7 @@ export const sampleEventSourceSinkbinding: FirehoseResult = { ], }; -export const sampleSourceKameletBinding: FirehoseResult = { +export const sampleSourceKameletBinding: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -868,7 +868,7 @@ export const sampleSourceKameletBinding: FirehoseResult = { ], }; -export const sampleSourceKafkaSink: FirehoseResult = { +export const sampleSourceKafkaSink: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -906,7 +906,7 @@ export const sampleSourceKafkaSink: FirehoseResult = { ], }; -export const sampleDomainMapping: FirehoseResult = { +export const sampleDomainMapping: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -928,7 +928,7 @@ export const sampleDomainMapping: FirehoseResult = { ], }; -export const sampleServices: FirehoseResult = { +export const sampleServices: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1007,7 +1007,7 @@ export const sampleServices: FirehoseResult = { ], }; -export const sampleClusterServiceVersions: FirehoseResult = { +export const sampleClusterServiceVersions: WatchK8sResultsObject = { loaded: true, loadError: '', data: [], @@ -1046,7 +1046,7 @@ export const knativeTopologyDataModel = { }, }; -export const sampleEventSourceDeployments: FirehoseResult = { +export const sampleEventSourceDeployments: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ @@ -1170,7 +1170,7 @@ export const EventIMCObj: EventChannelKind = { }, }; -export const sampleKnativeChannels: FirehoseResult = { +export const sampleKnativeChannels: WatchK8sResultsObject = { loaded: true, loadError: '', data: [EventIMCObj], @@ -1223,19 +1223,19 @@ export const EventTriggerObj: EventTriggerKind = { }, }; -const sampleBrokers: FirehoseResult = { +const sampleBrokers: WatchK8sResultsObject = { loaded: true, loadError: '', data: [EventBrokerObj], }; -const sampleTriggers: FirehoseResult = { +const sampleTriggers: WatchK8sResultsObject = { loaded: true, loadError: '', data: [EventTriggerObj], }; -const sampleKamelets: FirehoseResult = { +const sampleKamelets: WatchK8sResultsObject = { loaded: true, loadError: '', data: [], diff --git a/frontend/packages/knative-plugin/src/utils/__tests__/get-knative-resources.spec.ts b/frontend/packages/knative-plugin/src/utils/__tests__/get-knative-resources.spec.ts index d4b18847878..4fc7c1aeb03 100644 --- a/frontend/packages/knative-plugin/src/utils/__tests__/get-knative-resources.spec.ts +++ b/frontend/packages/knative-plugin/src/utils/__tests__/get-knative-resources.spec.ts @@ -1,5 +1,5 @@ import * as _ from 'lodash'; -import type { FirehoseResource } from '@console/internal/components/utils'; +import type { WatchK8sResourceWithProp } from '@console/internal/components/utils/types'; import { MockResources } from '@console/shared/src/utils/__tests__/test-resource-data'; import { knativeServiceObj, @@ -163,7 +163,7 @@ describe('Get knative resources', () => { describe('knative Serving Resources', () => { const SAMPLE_NAMESPACE = 'mynamespace'; it('expect knativeServingResource to return service with proper namespace', () => { - const serviceServingResource: FirehoseResource[] = knativeServingResourcesServices( + const serviceServingResource: WatchK8sResourceWithProp[] = knativeServingResourcesServices( SAMPLE_NAMESPACE, ); expect(serviceServingResource).toHaveLength(1); @@ -172,7 +172,7 @@ describe('Get knative resources', () => { }); it('expect knativeServingResourcesRevision to return revision with proper namespace', () => { - const revisionServingResource: FirehoseResource[] = knativeServingResourcesRevision( + const revisionServingResource: WatchK8sResourceWithProp[] = knativeServingResourcesRevision( SAMPLE_NAMESPACE, ); expect(revisionServingResource).toHaveLength(1); @@ -181,7 +181,7 @@ describe('Get knative resources', () => { }); it('expect knativeServingResourcesConfigurations to return configurations with proper namespace', () => { - const configServingResource: FirehoseResource[] = knativeServingResourcesConfigurations( + const configServingResource: WatchK8sResourceWithProp[] = knativeServingResourcesConfigurations( SAMPLE_NAMESPACE, ); expect(configServingResource).toHaveLength(1); @@ -190,7 +190,7 @@ describe('Get knative resources', () => { }); it('expect knativeServingResourcesRoutes to return routes with proper namespace', () => { - const routeServingResource: FirehoseResource[] = knativeServingResourcesRoutes( + const routeServingResource: WatchK8sResourceWithProp[] = knativeServingResourcesRoutes( SAMPLE_NAMESPACE, ); expect(routeServingResource).toHaveLength(1); diff --git a/frontend/packages/knative-plugin/src/utils/get-knative-resources.ts b/frontend/packages/knative-plugin/src/utils/get-knative-resources.ts index 3ce5937ecbd..b11c7bef42d 100644 --- a/frontend/packages/knative-plugin/src/utils/get-knative-resources.ts +++ b/frontend/packages/knative-plugin/src/utils/get-knative-resources.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash'; import type { WatchK8sResources, WatchK8sResourcesGeneric } from '@console/dynamic-plugin-sdk'; -import type { FirehoseResource } from '@console/internal/components/utils'; +import type { WatchK8sResourceWithProp } from '@console/internal/components/utils/types'; import type { K8sResourceKind, PodKind } from '@console/internal/module/k8s'; import { referenceForModel } from '@console/internal/module/k8s'; import { GLOBAL_OPERATOR_NS, KNATIVE_SERVING_LABEL } from '../const'; @@ -116,7 +116,7 @@ export const getKnativeServingServices = (dc: K8sResourceKind, props): KnativeIt return ksservices && ksservices.length > 0 ? { ksservices } : undefined; }; -export const knativeServingResourcesRevision = (namespace: string): FirehoseResource[] => { +export const knativeServingResourcesRevision = (namespace: string): WatchK8sResourceWithProp[] => { const knativeResource = [ { isList: true, @@ -129,7 +129,9 @@ export const knativeServingResourcesRevision = (namespace: string): FirehoseReso return knativeResource; }; -export const knativeServingResourcesConfigurations = (namespace: string): FirehoseResource[] => { +export const knativeServingResourcesConfigurations = ( + namespace: string, +): WatchK8sResourceWithProp[] => { const knativeResource = [ { isList: true, @@ -142,7 +144,7 @@ export const knativeServingResourcesConfigurations = (namespace: string): Fireho return knativeResource; }; -export const knativeServingResourcesRoutes = (namespace: string): FirehoseResource[] => { +export const knativeServingResourcesRoutes = (namespace: string): WatchK8sResourceWithProp[] => { const knativeResource = [ { isList: true, @@ -155,7 +157,7 @@ export const knativeServingResourcesRoutes = (namespace: string): FirehoseResour return knativeResource; }; -export const k8sServices = (namespace: string, limit?: number): FirehoseResource[] => { +export const k8sServices = (namespace: string, limit?: number): WatchK8sResourceWithProp[] => { const knativeResource = [ { isList: true, @@ -172,7 +174,7 @@ export const k8sServices = (namespace: string, limit?: number): FirehoseResource export const knativeServingResourcesServices = ( namespace: string, limit?: number, -): FirehoseResource[] => { +): WatchK8sResourceWithProp[] => { const knativeResource = [ { isList: true, @@ -186,7 +188,10 @@ export const knativeServingResourcesServices = ( return knativeResource; }; -export const knativeKafkaSinks = (namespace: string, limit?: number): FirehoseResource[] => { +export const knativeKafkaSinks = ( + namespace: string, + limit?: number, +): WatchK8sResourceWithProp[] => { const knativeResource = [ { isList: true, @@ -200,7 +205,9 @@ export const knativeKafkaSinks = (namespace: string, limit?: number): FirehoseRe return knativeResource; }; -export const knativeEventingResourcesSubscription = (namespace: string): FirehoseResource[] => { +export const knativeEventingResourcesSubscription = ( + namespace: string, +): WatchK8sResourceWithProp[] => { const knativeResource = [ { isList: true, @@ -216,7 +223,7 @@ export const knativeEventingResourcesSubscription = (namespace: string): Firehos export const knativeEventingResourcesBroker = ( namespace: string, limit?: number, -): FirehoseResource[] => { +): WatchK8sResourceWithProp[] => { const knativeResource = [ { isList: true, @@ -426,7 +433,7 @@ export const getTrafficByRevision = (revName: string, service: K8sResourceKind) }; }; -export const getSinkableResources = (namespace: string): FirehoseResource[] => { +export const getSinkableResources = (namespace: string): WatchK8sResourceWithProp[] => { return namespace ? [ ...k8sServices(namespace), diff --git a/frontend/packages/knative-plugin/src/utils/traffic-splitting-utils.ts b/frontend/packages/knative-plugin/src/utils/traffic-splitting-utils.ts index ab62c8b1a85..84da929ce8f 100644 --- a/frontend/packages/knative-plugin/src/utils/traffic-splitting-utils.ts +++ b/frontend/packages/knative-plugin/src/utils/traffic-splitting-utils.ts @@ -1,4 +1,4 @@ -import type { FirehoseResource } from '@console/internal/components/utils'; +import type { WatchK8sResourceWithProp } from '@console/internal/components/utils/types'; import type { K8sResourceKind, Patch } from '@console/internal/module/k8s'; import type { Traffic } from '../types'; import { @@ -25,7 +25,9 @@ export const trafficDataForPatch = (traffic: Traffic[], service: K8sResourceKind }, ]; -export const knativeServingResourcesTrafficSplitting = (namespace: string): FirehoseResource[] => [ +export const knativeServingResourcesTrafficSplitting = ( + namespace: string, +): WatchK8sResourceWithProp[] => [ ...knativeServingResourcesRevision(namespace), ...knativeServingResourcesConfigurations(namespace), ]; diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostDetailsPage.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostDetailsPage.tsx index 54759ce3710..9f3463ff12c 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostDetailsPage.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostDetailsPage.tsx @@ -2,8 +2,8 @@ import type { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { ResourceEventStream } from '@console/internal/components/events'; import { DetailsPage } from '@console/internal/components/factory'; -import type { FirehoseResource } from '@console/internal/components/utils'; import { navFactory } from '@console/internal/components/utils'; +import type { WatchK8sResourceWithProp } from '@console/internal/components/utils/types'; import { MachineModel, MachineSetModel, NodeModel } from '@console/internal/models'; import type { K8sResourceKind, @@ -45,7 +45,7 @@ const BareMetalHostDetailsPage: FC = (props) => { const { t } = useTranslation(); const [maintenanceModel] = useMaintenanceCapability(); const bmoEnabled = useFlag(BMO_ENABLED_FLAG); - const resources: FirehoseResource[] = [ + const resources: WatchK8sResourceWithProp[] = [ { kind: referenceForModel(MachineModel), namespaced: true, diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostsPage.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostsPage.tsx index 2d8704f9555..2d433ce8457 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostsPage.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/BareMetalHostsPage.tsx @@ -2,10 +2,16 @@ import type { FC } from 'react'; import type { TFunction } from 'i18next'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import { MultiListPage } from '@console/internal/components/factory'; -import type { FirehoseResource, FirehoseResult } from '@console/internal/components/utils'; +import type { WatchK8sResourceWithProp } from '@console/internal/components/utils/types'; import { MachineModel, MachineSetModel, NodeModel } from '@console/internal/models'; -import type { MachineKind, MachineSetKind, NodeKind } from '@console/internal/module/k8s'; +import type { + K8sResourceCommon, + MachineKind, + MachineSetKind, + NodeKind, +} from '@console/internal/module/k8s'; import { referenceForModel } from '@console/internal/module/k8s'; import { getName, createLookup, getNodeMachineName } from '@console/shared'; import { useMaintenanceCapability } from '../../hooks/useMaintenanceCapability'; @@ -19,19 +25,17 @@ import BareMetalHostsTable from './BareMetalHostsTable'; import { hostStatusFilter } from './table-filters'; type Resources = { - hosts: FirehoseResult; - machines: FirehoseResult; - machineSets: FirehoseResult; - nodes: FirehoseResult; - nodeMaintenances: FirehoseResult; + hosts: WatchK8sResultsObject; + machines: WatchK8sResultsObject; + machineSets: WatchK8sResultsObject; + nodes: WatchK8sResultsObject; + nodeMaintenances: WatchK8sResultsObject; }; const flattenResources = (resources: Resources) => { // TODO(jtomasek): Remove loaded check once ListPageWrapper_ is updated to call flatten only // when resources are loaded - const loaded = _.every(resources, (resource) => - resource.optional ? resource.loaded || !_.isEmpty(resource.loadError) : resource.loaded, - ); + const loaded = _.every(resources, (resource) => resource.loaded); if (loaded) { const { hosts, machines, machineSets, nodes, nodeMaintenances } = resources; @@ -100,7 +104,7 @@ const BareMetalHostsPage: FC = (props) => { const { t } = useTranslation(); const [model] = useMaintenanceCapability(); const { namespace } = props; - const resources: FirehoseResource[] = [ + const resources: WatchK8sResourceWithProp[] = [ { kind: referenceForModel(BareMetalHostModel), namespaced: true, diff --git a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx index 642ec7fd86d..19223cb4ba0 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.tsx @@ -23,7 +23,7 @@ import { sortable, wrappable } from '@patternfly/react-table'; import * as _ from 'lodash'; import { Trans, useTranslation } from 'react-i18next'; import { useParams, useLocation, Link } from 'react-router-dom-v5-compat'; -import type { WatchK8sResource } from '@console/dynamic-plugin-sdk'; +import type { WatchK8sResource, WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import { ResourceStatus, StatusIconAndText, @@ -36,7 +36,7 @@ import { Conditions, ConditionTypes } from '@console/internal/components/conditi import { ResourceEventStream } from '@console/internal/components/events'; import type { RowFunctionArgs, Flatten } from '@console/internal/components/factory'; import { DetailsPage, Table, TableData, MultiListPage } from '@console/internal/components/factory'; -import type { FirehoseResult, Page } from '@console/internal/components/utils'; +import type { Page } from '@console/internal/components/utils'; import { AsyncComponent, DOC_URL_OPERATORFRAMEWORK_SDK, @@ -1368,8 +1368,8 @@ export type ClusterServiceVersionListProps = { loaded: boolean; loadError?: string; data: ClusterServiceVersionKind[]; - subscriptions: FirehoseResult; - catalogSources: FirehoseResult; + subscriptions: WatchK8sResultsObject; + catalogSources: WatchK8sResultsObject; activeNamespace?: string; }; 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 37852fe287d..d8b03356bc1 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/k8s-resource.tsx @@ -6,8 +6,8 @@ import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom-v5-compat'; import type { Flatten, RowFunctionArgs } from '@console/internal/components/factory'; import { MultiListPage, Table, TableData } from '@console/internal/components/factory'; -import type { FirehoseResource } from '@console/internal/components/utils'; import { ResourceLink, ConsoleEmptyState } from '@console/internal/components/utils'; +import type { WatchK8sResourceWithProp } from '@console/internal/components/utils/types'; import { ConfigMapModel, DeploymentModel, @@ -148,8 +148,8 @@ export const Resources: FC = (props) => { const { plural } = useParams(); const providedAPI = providedAPIForReference(props.customData, plural); - const firehoseResources = (providedAPI?.resources ?? DEFAULT_RESOURCES).map( - ({ name, kind, version }): FirehoseResource => { + const watchResources = (providedAPI?.resources ?? DEFAULT_RESOURCES).map( + ({ name, kind, version }): WatchK8sResourceWithProp => { const group = name ? name.substring(name.indexOf('.') + 1) : ''; const reference = group ? referenceForGroupVersionKind(group)(version)(kind) : kind; const model = modelFor(reference); @@ -172,13 +172,13 @@ export const Resources: FC = (props) => { return ( kindForReference(kind), - items: firehoseResources.map(({ kind }) => ({ + items: watchResources.map(({ kind }) => ({ id: kindForReference(kind), title: kindForReference(kind), })), 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 d7bd4a05157..27139281b99 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx @@ -361,7 +361,7 @@ export const ProvidedAPIsPage = (props: ProvidedAPIsPageProps) => { const dispatch = useConsoleDispatch(); const [apiRefreshed, setAPIRefreshed] = useState(false); - // Map APIs provided by this CSV to Firehose resources. Exclude APIs that do not have a model. + // Map APIs provided by this CSV to watch resources. Exclude APIs that do not have a model. const providedAPIs = providedAPIsForCSV(obj); const owners = (ownerRefs: OwnerReference[], items: K8sResourceKind[]) => diff --git a/frontend/packages/topology/src/__tests__/topology-test-data.ts b/frontend/packages/topology/src/__tests__/topology-test-data.ts index 7980ffb6534..d0343c6452d 100644 --- a/frontend/packages/topology/src/__tests__/topology-test-data.ts +++ b/frontend/packages/topology/src/__tests__/topology-test-data.ts @@ -1,5 +1,5 @@ import type { Model } from '@patternfly/react-topology'; -import type { FirehoseResult } from '@console/internal/components/utils'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import type { EventKind } from '@console/internal/module/k8s'; import { CamelKameletBindingModel, KafkaSinkModel } from '@console/knative-plugin'; import { sampleDeployments } from '@console/shared/src/utils/__tests__/test-resource-data'; @@ -203,7 +203,7 @@ export const sampleHelmResourcesMap = { }, }; -export const sampleEventsResource: FirehoseResult = { +export const sampleEventsResource: WatchK8sResultsObject = { loaded: true, loadError: '', data: [ diff --git a/frontend/public/components/__tests__/storage-class-form.spec.tsx b/frontend/public/components/__tests__/storage-class-form.spec.tsx index 8f538862c84..aa6a636ddbf 100644 --- a/frontend/public/components/__tests__/storage-class-form.spec.tsx +++ b/frontend/public/components/__tests__/storage-class-form.spec.tsx @@ -11,11 +11,6 @@ jest.mock('react-router-dom-v5-compat', () => ({ useNavigate: jest.fn(), })); -// Mock Firehose -jest.mock('../utils/firehose', () => ({ - Firehose: ({ children }) => children, -})); - describe('StorageClassForm', () => { let onClose: jest.Mock; diff --git a/frontend/public/components/cron-job.tsx b/frontend/public/components/cron-job.tsx index 7a4c462e7de..a7d47d92441 100644 --- a/frontend/public/components/cron-job.tsx +++ b/frontend/public/components/cron-job.tsx @@ -412,6 +412,9 @@ export const CronJobsPage: FC = (props) => ( export const CronJobsDetailsPage: FC = (props) => { const customActionMenu = (kindObj, obj) => { + if (!kindObj || !obj) { + return null; + } const resourceKind = referenceForModel(kindObj); const context = { [resourceKind]: obj }; return ( diff --git a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/health-item.tsx b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/health-item.tsx index 091f0cb37da..012510f4ee9 100644 --- a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/health-item.tsx +++ b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/health-item.tsx @@ -28,7 +28,6 @@ import { import { useDynamicK8sWatchResources } from '@console/shared/src/hooks/useDynamicK8sWatchResources'; import { useDashboardResources } from '@console/shared/src/hooks/useDashboardResources'; import { K8sKind } from '../../../../module/k8s'; -import { FirehoseResourcesResult } from '../../../utils/types'; import { AsyncComponent, LazyLoader } from '../../../utils/async'; import { resourcePath } from '../../../utils/resource-link'; import { useK8sWatchResource, useK8sWatchResources } from '../../../utils/k8s-watch-hook'; @@ -310,6 +309,6 @@ type ResourceHealthItemProps = { }; type OperatorsPopupProps = { - resources: FirehoseResourcesResult; + resources: WatchK8sResults; operatorSubsystems: ResolvedExtension['properties'][]; }; diff --git a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/inventory-card.tsx b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/inventory-card.tsx index 2258f1f205a..c62f5ab8ae8 100644 --- a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/inventory-card.tsx +++ b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/inventory-card.tsx @@ -28,7 +28,7 @@ const mergeItems = ( (item) => replacements.find((r) => r.properties.model === item.properties.model) || item, ); -const getFirehoseResource = (model: K8sKind) => ({ +const getWatchResource = (model: K8sKind) => ({ isList: true, kind: model.crd ? referenceForModel(model) : model.kind, prop: 'resource', @@ -36,7 +36,7 @@ const getFirehoseResource = (model: K8sKind) => ({ const ClusterInventoryItem = memo( ({ model, resolvedMapper, mapperLoader, additionalResources }) => { - const mainResource = useMemo(() => getFirehoseResource(model), [model]); + const mainResource = useMemo(() => getWatchResource(model), [model]); const otherResources = useMemo(() => additionalResources || {}, [additionalResources]); const [mapper, setMapper] = useState(); const [resourceData, resourceLoaded, resourceLoadError] = useK8sWatchResource< diff --git a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/utils.ts b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/utils.ts index b085d927ea2..9452afb38ec 100644 --- a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/utils.ts +++ b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/utils.ts @@ -1,9 +1,9 @@ -import type { FirehoseResource } from '../../../utils/types'; +import type { WatchK8sResource } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; -export const uniqueResource = ( - resource: FirehoseResource, +export const uniqueResource = ( + resource: T, prefix: string | number, -): FirehoseResource => ({ +): T => ({ ...resource, prop: `${prefix}-${resource.prop}`, }); diff --git a/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx b/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx index e706f95035b..8d3c6b7573c 100644 --- a/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx +++ b/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx @@ -24,7 +24,6 @@ import { getPVCStatusGroups, getVSStatusGroups, } from '@console/shared/src/components/dashboard/inventory-card/utils'; -import type { FirehoseResource } from '../../utils/types'; import { useAccessReview } from '../../utils/rbac'; import { K8sKind, @@ -38,6 +37,7 @@ import { DashboardsProjectOverviewInventoryItem, isDashboardsProjectOverviewInventoryItem, K8sResourceCommon, + WatchK8sResource, WatchK8sResources, ProjectOverviewInventoryItem, isProjectOverviewInventoryItem, @@ -45,7 +45,10 @@ import { import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; import { ErrorBoundary } from '@console/shared/src/components/error'; -const createFirehoseResource = (model: K8sKind, projectName: string): FirehoseResource => ({ +const createWatchResource = ( + model: K8sKind, + projectName: string, +): WatchK8sResource & { prop: string } => ({ kind: model.crd ? referenceForModel(model) : model.kind, isList: true, prop: 'resource', @@ -66,7 +69,7 @@ const ProjectInventoryItem: React.FC = ({ return; } - const resource = createFirehoseResource(model, projectName); + const resource = createWatchResource(model, projectName); const { prop, ...resourceConfig } = resource; watchResource(prop, resourceConfig); if (additionalResources) { @@ -90,9 +93,9 @@ const ProjectInventoryItem: React.FC = ({ const additionalResourcesData = additionalResources ? additionalResources.reduce((acc, r) => { - acc[r.prop] = _.get(resources[r.prop], 'data'); + acc[r.prop] = _.get(resources[r.prop], 'data', []) as K8sResourceCommon[]; return acc; - }, {}) + }, {} as { [key: string]: K8sResourceCommon[] }) : {}; const additionalResourcesLoaded = additionalResources ? additionalResources @@ -202,7 +205,7 @@ type ProjectInventoryItemProps = { projectName: string; model: K8sKind; mapper?: StatusGroupMapper; - additionalResources?: FirehoseResource[]; + additionalResources?: (WatchK8sResource & { prop: string })[]; additionalDynamicResources?: WatchK8sResources<{ [key: string]: K8sResourceCommon[]; }>; diff --git a/frontend/public/components/factory/__tests__/list-page.spec.tsx b/frontend/public/components/factory/__tests__/list-page.spec.tsx index 6f061fb1e2f..0c2a6eb36a9 100644 --- a/frontend/public/components/factory/__tests__/list-page.spec.tsx +++ b/frontend/public/components/factory/__tests__/list-page.spec.tsx @@ -84,12 +84,12 @@ describe('TextFilter component', () => { describe('FireMan component', () => { it('does not render title when not provided', () => { - renderWithProviders(); + renderWithProviders(); expect(screen.queryByText('My pods')).not.toBeInTheDocument(); }); it('renders title when provided', () => { - renderWithProviders(); + renderWithProviders(); expect(screen.getByText('My pods')).toBeVisible(); }); @@ -97,13 +97,7 @@ describe('FireMan component', () => { const createProps = {}; renderWithProviders( - , + , ); const button = screen.getByRole('button', { name: 'Create Pod' }); diff --git a/frontend/public/components/factory/details.tsx b/frontend/public/components/factory/details.tsx index bbe218a7b0e..53e02f4563a 100644 --- a/frontend/public/components/factory/details.tsx +++ b/frontend/public/components/factory/details.tsx @@ -19,11 +19,11 @@ import { DetailPageBreadCrumbs, } from '@console/dynamic-plugin-sdk/src/extensions/breadcrumbs'; import { - FirehoseResult, K8sResourceKindReference, K8sResourceKind, K8sResourceCommon, WatchK8sResource, + WatchK8sResultsObject, } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; import { HorizontalNav } from '../utils/horizontal-nav'; import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; @@ -33,7 +33,6 @@ import { ConnectedPageHeadingProps, KebabOptionsCreator, } from '../utils/headings'; -import { FirehoseResource } from '../utils/types'; import { K8sKind } from '../../module/k8s/types'; import { getReferenceForModel as referenceForModel } from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-ref'; import { referenceForExtensionModel } from '../../module/k8s/k8s'; @@ -103,7 +102,7 @@ export const DetailsPage = withFallback(({ pages = [], ...prop }, []); let allPages = [...pages, ...pluginPages]; allPages = allPages.length ? allPages : null; - const objResource = useMemo( + const objResource = useMemo( () => ({ kind: props.kind, name: props.name, @@ -195,7 +194,7 @@ export const DetailsPage = withFallback(({ pages = [], ...prop }, ErrorBoundaryFallbackPage); export type DetailsPageProps = { - obj?: FirehoseResult; + obj?: WatchK8sResultsObject; title?: string | JSX.Element; titleFunc?: (obj: K8sResourceKind) => string | JSX.Element; menuActions?: KebabAction[] | KebabOptionsCreator; @@ -210,7 +209,7 @@ export type DetailsPageProps = { label?: string; name?: string; namespace?: string; - resources?: FirehoseResource[]; + resources?: (WatchK8sResource & { prop?: string })[]; breadcrumbsFor?: ( obj: K8sResourceKind, ) => ({ name: string; path: string } | { name: string; path: Location })[]; diff --git a/frontend/public/components/factory/list-page.tsx b/frontend/public/components/factory/list-page.tsx index c210c5b75a1..05ff2abf594 100644 --- a/frontend/public/components/factory/list-page.tsx +++ b/frontend/public/components/factory/list-page.tsx @@ -24,7 +24,11 @@ import { K8sKind } from '../../module/k8s/types'; import { getReferenceForModel as referenceForModel } from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-ref'; import { Selector } from '@console/dynamic-plugin-sdk/src/api/common-types'; import { useK8sWatchResources } from '../utils/k8s-watch-hook'; -import { FirehoseResource, FirehoseResourcesResult, FirehoseResultObject } from '../utils/types'; +import type { + WatchK8sResults, + ResourcesObject, +} from '@console/dynamic-plugin-sdk/src/extensions/console-types'; + import { inject, kindObj } from '../utils/inject'; import { makeQuery, @@ -59,8 +63,6 @@ type ListPageWrapperProps = { hideLabelFilter?: boolean; columnLayout?: ColumnLayout; name?: string; - /** @deprecated - use watchedResources instead */ - resources?: FirehoseResourcesResult; /** Resources fetched via useK8sWatchResources */ watchedResources?: Record>; loaded?: boolean; @@ -91,7 +93,6 @@ export const ListPageWrapper: FC = (props) => { hideLabelFilter, columnLayout, name, - resources, watchedResources, nameFilter, omitFilterToolbar, @@ -105,10 +106,7 @@ export const ListPageWrapper: FC = (props) => { } }, [dispatch, nameFilter, memoizedIds]); - // TODO: Remove the resources prop and the fallback ?? resources after all components are migrated from Firehose to hooks. - // Use watchedResources (from useK8sWatchResources) if available, fallback to resources (from Firehose) - const resourceData = watchedResources ?? resources; - const data = flatten ? flatten(resourceData) : []; + const data = flatten ? flatten(watchedResources) : []; const Filter = ( = (props) => { const { - resources, + reduxIDs = [], textFilter, canCreate, createAccessReview, @@ -174,35 +172,15 @@ export const FireMan: FC = (p } = props; const navigate = useNavigate(); - const [reduxIDs, setReduxIDs] = useState([]); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [expand] = useState(); useEffect(() => { const params = new URLSearchParams(window.location.search); params.forEach((v, k) => applyFilter(k, v)); - - const reduxId = resources.map((r) => - makeReduxID(kindObj(r.kind), makeQuery(r.namespace, r.selector, r.fieldSelector, r.name)), - ); - setReduxIDs(reduxId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - const reduxId = resources.map((r) => - makeReduxID(kindObj(r.kind), makeQuery(r.namespace, r.selector, r.fieldSelector, r.name)), - ); - - if (_.isEqual(reduxId, reduxIDs)) { - return; - } - - // reapply filters to the new list... - setReduxIDs(reduxId); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [resources]); - const updateURL = (filterName: string, options: any) => { if (filterName !== textFilter) { // TODO (ggreer): support complex filters (objects, not just strings) @@ -297,7 +275,6 @@ export const FireMan: FC = (p {inject(props.children, { - resources, expand, reduxIDs, applyFilter, @@ -309,9 +286,9 @@ export const FireMan: FC = (p FireMan.displayName = 'FireMan'; export type Flatten< - F extends FirehoseResultObject = { [key: string]: K8sResourceCommon | K8sResourceCommon[] }, + F extends ResourcesObject = { [key: string]: K8sResourceCommon | K8sResourceCommon[] }, R = any -> = (resources: FirehoseResourcesResult) => R; +> = (resources: WatchK8sResults) => R; export type ListPageProps = PageCommonProps & { kind: string; @@ -478,7 +455,7 @@ export type MultiListPageProps = PageCommonProps & { hideTextFilter?: boolean; helpText?: ReactNode; helpAlert?: ReactNode; - resources: (Omit & { prop?: FirehoseResource['prop'] })[]; + resources: (WatchK8sResource & { prop?: string })[]; staticFilters?: { key: string; value: string }[]; nameFilter?: string; omitFilterToolbar?: boolean; @@ -518,30 +495,19 @@ export const MultiListPage: FC = (props) => { const { t } = useTranslation(); - // Build resources configuration for FireMan (needs prop for redux IDs) - const k8sResources = useMemo( - () => - _.map(props.resources, (r) => ({ - ...r, - isList: r.isList !== undefined ? r.isList : true, - namespace: r.namespaced ? namespace : r.namespace, - prop: r.prop || r.kind, - })), - [props.resources, namespace], - ); - // Build watch resources configuration for useK8sWatchResources const watchResources = useMemo(() => { if (mock) { return {}; } - return k8sResources.reduce((acc, r) => { + return props.resources.reduce((acc, r) => { const key = r.prop || r.kind; + const resourceNamespace = r.namespaced ? namespace : r.namespace; acc[key] = { kind: r.kind, name: r.name, - namespace: r.namespace, - isList: r.isList, + namespace: resourceNamespace, + isList: r.isList !== undefined ? r.isList : true, selector: r.selector, fieldSelector: r.fieldSelector, limit: r.limit, @@ -550,7 +516,20 @@ export const MultiListPage: FC = (props) => { }; return acc; }, {} as Record); - }, [k8sResources, mock]); + }, [props.resources, namespace, mock]); + + // Build redux IDs for filter management + const reduxIDs = useMemo( + () => + props.resources.map((r) => { + const resourceNamespace = r.namespaced ? namespace : r.namespace; + return makeReduxID( + kindObj(r.kind), + makeQuery(resourceNamespace, r.selector, r.fieldSelector, r.name), + ); + }), + [props.resources, namespace], + ); const watchedResources = useK8sWatchResources< Record @@ -577,14 +556,14 @@ export const MultiListPage: FC = (props) => { filterLabel={filterLabel || t('public~by name')} helpText={helpText} helpAlert={helpAlert} - resources={mock ? [] : k8sResources} + reduxIDs={mock ? [] : reduxIDs} textFilter={textFilter} title={showTitle ? title : undefined} badge={badge} > = (props) => { omitFilterToolbar={omitFilterToolbar} watchedResources={mock ? {} : watchedResources} loaded={loaded} + reduxIDs={reduxIDs} /> ); diff --git a/frontend/public/components/sidebars/resource-sidebar-samples.tsx b/frontend/public/components/sidebars/resource-sidebar-samples.tsx index 2512f66ba44..06d091c1237 100644 --- a/frontend/public/components/sidebars/resource-sidebar-samples.tsx +++ b/frontend/public/components/sidebars/resource-sidebar-samples.tsx @@ -11,8 +11,8 @@ import { PasteIcon } from '@patternfly/react-icons/dist/esm/icons/paste-icon'; import { Sample } from '@console/shared/src/hooks/useResourceSidebarSamples'; import { useTranslation } from 'react-i18next'; +import type { K8sResourceCommon, WatchK8sResultsObject } from '@console/dynamic-plugin-sdk'; import { K8sKind, referenceFor } from '../../module/k8s'; -import { FirehoseResult } from '../utils/types'; const ResourceSidebarSample: FC = ({ sample, @@ -214,6 +214,6 @@ type ResourceSidebarSamplesProps = { samples: Sample[]; loadSampleYaml: LoadSampleYaml; downloadSampleYaml: DownloadSampleYaml; - yamlSamplesList?: FirehoseResult; + yamlSamplesList?: WatchK8sResultsObject; kindObj: K8sKind; }; diff --git a/frontend/public/components/utils/__tests__/firehose.data.tsx b/frontend/public/components/utils/__tests__/firehose.data.tsx deleted file mode 100644 index e90789d5ad0..00000000000 --- a/frontend/public/components/utils/__tests__/firehose.data.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -export { PodModel } from '../../../models'; - -export const podData = { - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name: 'my-pod', - namespace: 'my-namespace', - resourceVersion: '123', - }, -}; - -export const podList = { - apiVersion: 'v1', - kind: 'PodList', - items: ['my-pod1', 'my-pod2', 'my-pod3'].map((name) => ({ - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name, - namespace: 'my-namespace', - resourceVersion: '123', - }, - })), - metadata: { resourceVersion: '123' }, -}; - -export const firehoseChildPropsWithoutModels = { - inFlight: false, - k8sModels: ImmutableMap({}), - reduxIDs: [], - resources: {}, - loaded: true, - loadError: undefined, - filters: {}, - watchK8sList: expect.any(Function), - watchK8sObject: expect.any(Function), - stopK8sWatch: expect.any(Function), -}; diff --git a/frontend/public/components/utils/__tests__/firehose.spec.tsx b/frontend/public/components/utils/__tests__/firehose.spec.tsx deleted file mode 100644 index 415bb4bccc5..00000000000 --- a/frontend/public/components/utils/__tests__/firehose.spec.tsx +++ /dev/null @@ -1,1601 +0,0 @@ -import type { ReactNode, FC } from 'react'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import { combineReducers, createStore, applyMiddleware } from 'redux'; -import thunk from 'redux-thunk'; -import { Provider } from 'react-redux'; -import { act, cleanup, render } from '@testing-library/react'; -import { SDKReducers } from '@console/dynamic-plugin-sdk/src/app'; -import { k8sList, k8sGet } from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-resource'; -import { k8sWatch } from '@console/dynamic-plugin-sdk/src/utils/k8s'; -import { WatchK8sResources } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; -import { useK8sWatchResources } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources'; -import { receivedResources } from '../../../actions/k8s'; -import { processReduxId, Firehose } from '../firehose'; -import { PodModel, podData, podList, firehoseChildPropsWithoutModels } from './firehose.data'; - -// Mock network calls -jest.mock('@console/dynamic-plugin-sdk/src/utils/k8s/k8s-resource', () => ({ - k8sList: jest.fn(() => {}), - k8sGet: jest.fn(), -})); -jest.mock('@console/dynamic-plugin-sdk/src/utils/k8s', () => ({ - ...jest.requireActual('@console/dynamic-plugin-sdk/src/utils/k8s'), - k8sWatch: jest.fn(), -})); -const k8sListMock = k8sList as jest.Mock; -const k8sGetMock = k8sGet as jest.Mock; -const k8sWatchMock = k8sWatch as jest.Mock; - -// Redux wrapper -let store; - -interface WrapperProps { - children?: ReactNode; -} - -const Wrapper: FC = ({ children }) => {children}; - -describe('processReduxId', () => { - const k8s = ImmutableMap({ - ['Pods']: ImmutableMap({ - data: ImmutableList( - ['my-pod1', 'my-pod2', 'my-pod3'].map((name) => - ImmutableMap({ - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name, - namespace: 'my-namespace', - resourceVersion: '123', - }, - }), - ), - ), - }), - ['Pods~~~my-pod']: ImmutableMap({ - data: ImmutableMap({ - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name: 'my-pod', - namespace: 'my-namespace', - resourceVersion: '123', - }, - }), - }), - }); - - it('should return an empty object when reduxID prop is missing', () => { - const props = { kind: 'UnknownKind' }; - expect(processReduxId({ k8s }, props)).toEqual({}); - }); - - it("should return an object without data when extract a list which doesn't exist", () => { - const props = { - reduxID: 'Unknown', - kind: 'Pod', - isList: true, - }; - expect(processReduxId({ k8s }, props)).toEqual({ - data: undefined, - filters: {}, - kind: 'Pod', - loadError: undefined, - loaded: undefined, - optional: undefined, - selected: undefined, - }); - }); - - it("should return an empty object when extract a single item which doesn't exist", () => { - const props = { - reduxID: 'Unknown', - kind: 'Pod', - isList: false, - }; - expect(processReduxId({ k8s }, props)).toEqual({}); - }); - - it('should return an Firehose object with data when extract a list', () => { - const props = { - reduxID: 'Pods', - kind: 'Pod', - isList: true, - }; - expect(processReduxId({ k8s }, props)).toEqual({ - kind: 'Pod', - data: [ - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod1', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod2', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod3', namespace: 'my-namespace', resourceVersion: '123' }, - }, - ], - filters: {}, - loadError: undefined, - loaded: undefined, - optional: undefined, - selected: undefined, - }); - }); - - it('should return the same object twice when calling it twice for a list', () => { - const props = { - reduxID: 'Pods', - kind: 'Pod', - isList: true, - }; - const firstTime = processReduxId({ k8s }, props); - const secondTime = processReduxId({ k8s }, props); - // Exact JSON is tested above. - // It returns always a new result object - expect(firstTime).not.toBe(secondTime); - // But at least the data should be the same - expect(firstTime.data).toBe(secondTime.data); - }); - - it('should return an Firehose object with data when extract a single item', () => { - const props = { - reduxID: 'Pods~~~my-pod', - kind: 'Pod', - isList: false, - }; - expect(processReduxId({ k8s }, props)).toEqual({ - data: { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod', namespace: 'my-namespace', resourceVersion: '123' }, - }, - optional: undefined, - }); - }); - - it('should return the same object twice when calling it twice for a single item', () => { - const props = { - reduxID: 'Pods~~~my-pod', - kind: 'Pod', - isList: false, - }; - const firstTime = processReduxId({ k8s }, props); - const secondTime = processReduxId({ k8s }, props); - // Exact JSON is tested above. - // It returns always a new result object - // And it could not be the same because optional parameter could change! - expect(firstTime).not.toBe(secondTime); - // But at least the data should be the same - expect(firstTime.data).toBe(secondTime.data); - }); - - it('should return different data for isList true and false, but same data when calling multiple times', () => {}); -}); - -describe('Firehose', () => { - // Object under test - const resourceUpdate = jest.fn(); - const Child: FC = (props) => { - resourceUpdate(props); - return null; - }; - - beforeEach(() => { - // Init k8s redux store with just one model - store = createStore(combineReducers(SDKReducers), {}, applyMiddleware(thunk)); - store.dispatch( - receivedResources({ - models: [PodModel], - adminResources: [], - allResources: [], - configResources: [], - clusterOperatorConfigResources: [], - namespacedSet: null, - safeResources: [], - groupVersionMap: {}, - }), - ); - - jest.useFakeTimers({ legacyFakeTimers: true }); - jest.resetAllMocks(); - - k8sListMock.mockReturnValue(Promise.resolve(podList)); - k8sGetMock.mockReturnValue(Promise.resolve(podData)); - const wsMock = { - onclose: () => wsMock, - ondestroy: () => wsMock, - onbulkmessage: () => wsMock, - destroy: () => wsMock, - }; - k8sWatchMock.mockReturnValue(wsMock); - }); - - afterEach(async () => { - // Ensure that there is no timer left which triggers a rerendering - await act(async () => { - jest.runAllTimers(); - }); - - cleanup(); - - // Ensure that there is no unexpected api calls - expect(k8sListMock).toHaveBeenCalledTimes(0); - expect(k8sGetMock).toHaveBeenCalledTimes(0); - expect(k8sWatchMock).toHaveBeenCalledTimes(0); - expect(resourceUpdate).toHaveBeenCalledTimes(0); - - jest.clearAllTimers(); - jest.useRealTimers(); - }); - - it('should return an empty object when reduxID prop is missing (also when rerender or unmount)', async () => { - const { rerender, unmount } = render( - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - rerender( - - - - - , - ); - unmount(); - - expect(resourceUpdate).toHaveBeenCalledTimes(2); - expect(resourceUpdate.mock.calls[0][0]).toEqual(firehoseChildPropsWithoutModels); - expect(resourceUpdate.mock.calls[1][0]).toEqual(firehoseChildPropsWithoutModels); - resourceUpdate.mockClear(); - }); - - it('should fetch and update child props when requesting a list of resources successfully', async () => { - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: true, - namespace: 'my-namespace', - }, - ]; - const { rerender, unmount } = render( - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - expect(k8sListMock).toHaveBeenCalledTimes(1); - expect(k8sListMock.mock.calls[0]).toEqual([ - PodModel, - { limit: 250, ns: 'my-namespace' }, - true, - {}, - ]); - k8sListMock.mockClear(); - - // Expect initial render child-props - const podsNotLoadedYet = { - kind: 'Pod', - data: [], - loaded: false, - loadError: '', - filters: {}, - selected: null, - optional: undefined, - }; - const podsNotLoadedYetProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: ['core~v1~Pod---{"ns":"my-namespace"}'], - loaded: false, - // Yes, same data twice at the moment. - pods: podsNotLoadedYet, - resources: { pods: podsNotLoadedYet }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(1); - expect(resourceUpdate.mock.calls[0][0]).toEqual(podsNotLoadedYetProps); - - // Finish API call - await act(async () => { - jest.runAllTimers(); - }); - - // Expect updated child-props - const podsLoaded = { - kind: 'Pod', - data: ['my-pod1', 'my-pod2', 'my-pod3'].map((name) => ({ - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name, - namespace: 'my-namespace', - resourceVersion: '123', - }, - })), - loaded: true, - loadError: '', - filters: {}, - selected: null, - optional: undefined, - }; - const podsLoadedProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: ['core~v1~Pod---{"ns":"my-namespace"}'], - loaded: true, - // Yes, same data twice at the moment. - pods: podsLoaded, - resources: { pods: podsLoaded }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(2); - expect(resourceUpdate.mock.calls[1][0]).toEqual(podsLoadedProps); - - // Check rerender and unmount - rerender( - - - - - , - ); - unmount(); - expect(resourceUpdate).toHaveBeenCalledTimes(3); - expect(resourceUpdate.mock.calls[2][0]).toEqual(podsLoadedProps); - - resourceUpdate.mockClear(); - }); - - it('should fetch and update child props when requesting a single resource successfully', async () => { - const resources = [ - { - prop: 'pod', - kind: 'Pod', - namespace: 'my-namespace', - name: 'my-pod', - }, - ]; - const { rerender, unmount } = render( - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - expect(k8sGetMock).toHaveBeenCalledTimes(1); - expect(k8sGetMock.mock.calls[0]).toEqual([PodModel, 'my-pod', 'my-namespace', {}, {}]); - k8sGetMock.mockClear(); - - // Expect initial render child-props - const podNotLoadedYet = { - data: {}, - loaded: false, - loadError: '', - optional: undefined, - }; - const podsNotLoadedYetProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: ['core~v1~Pod---{"ns":"my-namespace","name":"my-pod"}'], - loaded: false, - // Yes, same data twice at the moment. - pod: podNotLoadedYet, - resources: { pod: podNotLoadedYet }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(1); - expect(resourceUpdate.mock.calls[0][0]).toEqual(podsNotLoadedYetProps); - - // Finish API call - await act(async () => { - jest.runAllTimers(); - }); - - // Expect updated child-props - const podLoaded = { - data: { - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name: 'my-pod', - namespace: 'my-namespace', - resourceVersion: '123', - }, - }, - loaded: true, - loadError: '', - optional: undefined, - }; - const podLoadedProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: ['core~v1~Pod---{"ns":"my-namespace","name":"my-pod"}'], - loaded: true, - // Yes, same data twice at the moment. - pod: podLoaded, - resources: { pod: podLoaded }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(2); - expect(resourceUpdate.mock.calls[1][0]).toEqual(podLoadedProps); - - // Check rerender and unmount - rerender( - - - - - , - ); - unmount(); - expect(resourceUpdate).toHaveBeenCalledTimes(3); - expect(resourceUpdate.mock.calls[2][0]).toEqual(podLoadedProps); - - resourceUpdate.mockClear(); - }); - - it('should fetch and update child props when requesting a list of resources fails', async () => { - k8sListMock.mockReturnValue(Promise.reject(new Error('Network issue'))); - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: true, - namespace: 'my-namespace', - }, - ]; - const { rerender, unmount } = render( - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - expect(k8sListMock).toHaveBeenCalledTimes(1); - expect(k8sListMock.mock.calls[0]).toEqual([ - PodModel, - { limit: 250, ns: 'my-namespace' }, - true, - {}, - ]); - k8sListMock.mockClear(); - - // Expect initial render child-props - const podsNotLoadedYet = { - kind: 'Pod', - data: [], - loaded: false, - loadError: '', - filters: {}, - selected: null, - optional: undefined, - }; - const podsNotLoadedYetProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: ['core~v1~Pod---{"ns":"my-namespace"}'], - loaded: false, - // Yes, same data twice at the moment. - pods: podsNotLoadedYet, - resources: { pods: podsNotLoadedYet }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(1); - expect(resourceUpdate.mock.calls[0][0]).toEqual(podsNotLoadedYetProps); - - // Finish API call - await act(async () => { - jest.runAllTimers(); - }); - - // Expect updated child-props - const podsLoaded = { - kind: 'Pod', - data: [], - loaded: false, - loadError: new Error('Network issue'), - filters: {}, - selected: null, - optional: undefined, - }; - const podsLoadedProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: ['core~v1~Pod---{"ns":"my-namespace"}'], - loaded: false, - loadError: new Error('Network issue'), - // Yes, same data twice at the moment. - pods: podsLoaded, - resources: { pods: podsLoaded }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(2); - expect(resourceUpdate.mock.calls[1][0]).toEqual(podsLoadedProps); - - // Check rerender and unmount - rerender( - - - - - , - ); - unmount(); - expect(resourceUpdate).toHaveBeenCalledTimes(3); - expect(resourceUpdate.mock.calls[2][0]).toEqual(podsLoadedProps); - - resourceUpdate.mockClear(); - }); - - it('should fetch and update child props when requesting a single resource fails', async () => { - k8sGetMock.mockReturnValue(Promise.reject(new Error('Network issue'))); - const resources = [ - { - prop: 'pod', - kind: 'Pod', - namespace: 'my-namespace', - name: 'my-pod', - }, - ]; - const { rerender, unmount } = render( - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - expect(k8sGetMock).toHaveBeenCalledTimes(1); - expect(k8sGetMock.mock.calls[0]).toEqual([PodModel, 'my-pod', 'my-namespace', {}, {}]); - k8sGetMock.mockClear(); - - // Expect initial render child-props - const podNotLoadedYet = { - data: {}, - loaded: false, - loadError: '', - optional: undefined, - }; - const podsNotLoadedYetProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: ['core~v1~Pod---{"ns":"my-namespace","name":"my-pod"}'], - loaded: false, - // Yes, same data twice at the moment. - pod: podNotLoadedYet, - resources: { pod: podNotLoadedYet }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(1); - expect(resourceUpdate.mock.calls[0][0]).toEqual(podsNotLoadedYetProps); - - // Finish API call - await act(async () => { - jest.runAllTimers(); - }); - - // Expect updated child-props - const podLoaded = { - data: {}, - loaded: false, - loadError: new Error('Network issue'), - optional: undefined, - }; - const podLoadedProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: ['core~v1~Pod---{"ns":"my-namespace","name":"my-pod"}'], - loaded: false, - loadError: new Error('Network issue'), - // Yes, same data twice at the moment. - pod: podLoaded, - resources: { pod: podLoaded }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(2); - expect(resourceUpdate.mock.calls[1][0]).toEqual(podLoadedProps); - - // Check rerender and unmount - rerender( - - - - - , - ); - unmount(); - expect(resourceUpdate).toHaveBeenCalledTimes(3); - expect(resourceUpdate.mock.calls[2][0]).toEqual(podLoadedProps); - - resourceUpdate.mockClear(); - }); - - it('should set the props to all childrens and fetch the data just once', async () => { - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: true, - namespace: 'my-namespace', - }, - { - prop: 'pod', - kind: 'Pod', - namespace: 'my-namespace', - name: 'my-pod', - }, - ]; - render( - - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - // Assert that API calls are just triggered once - expect(k8sListMock).toHaveBeenCalledTimes(1); - expect(k8sListMock.mock.calls[0]).toEqual([ - PodModel, - { limit: 250, ns: 'my-namespace' }, - true, - {}, - ]); - k8sListMock.mockClear(); - expect(k8sGetMock).toHaveBeenCalledTimes(1); - expect(k8sGetMock.mock.calls[0]).toEqual([PodModel, 'my-pod', 'my-namespace', {}, {}]); - k8sGetMock.mockClear(); - - // Expect initial render child-props - const podsNotLoadedYet = { - kind: 'Pod', - data: [], - loaded: false, - loadError: '', - filters: {}, - selected: null, - optional: undefined, - }; - const podNotLoadedYet = { - data: {}, - loaded: false, - loadError: '', - optional: undefined, - }; - const notLoadedYetProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: [ - 'core~v1~Pod---{"ns":"my-namespace"}', - 'core~v1~Pod---{"ns":"my-namespace","name":"my-pod"}', - ], - loaded: false, - // Yes, same data twice at the moment. - pods: podsNotLoadedYet, - pod: podNotLoadedYet, - resources: { pods: podsNotLoadedYet, pod: podNotLoadedYet }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(2); - expect(resourceUpdate.mock.calls[0][0]).toEqual(notLoadedYetProps); - expect(resourceUpdate.mock.calls[1][0]).toEqual(notLoadedYetProps); - - // Finish API call - await act(async () => { - jest.runAllTimers(); - }); - - // Expect updated child-props - const podsLoaded = { - kind: 'Pod', - data: ['my-pod1', 'my-pod2', 'my-pod3'].map((name) => ({ - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name, - namespace: 'my-namespace', - resourceVersion: '123', - }, - })), - loaded: true, - loadError: '', - filters: {}, - selected: null, - optional: undefined, - }; - const podLoaded = { - data: { - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name: 'my-pod', - namespace: 'my-namespace', - resourceVersion: '123', - }, - }, - loaded: true, - loadError: '', - optional: undefined, - }; - const loadedProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: [ - 'core~v1~Pod---{"ns":"my-namespace"}', - 'core~v1~Pod---{"ns":"my-namespace","name":"my-pod"}', - ], - loaded: true, - // Yes, same data twice at the moment. - pods: podsLoaded, - pod: podLoaded, - resources: { pods: podsLoaded, pod: podLoaded }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(6); - // skip rerendering 2 so that both data sets are loaded - expect(resourceUpdate.mock.calls[4][0]).toEqual(loadedProps); - expect(resourceUpdate.mock.calls[5][0]).toEqual(loadedProps); - const propsChildA = resourceUpdate.mock.calls[4][0]; - const propsChildB = resourceUpdate.mock.calls[5][0]; - resourceUpdate.mockClear(); - - // Check that all data shares the same identity for the loaded data. - expect(propsChildA).toEqual(propsChildB); - expect(propsChildA).not.toBe(propsChildB); // TODO: These props could be the same, or? - - // pods 'resource' object (with data, loaded, etc.) object - expect(propsChildA.pods).toBe(propsChildB.pods); - expect(propsChildA.pods.data).toBe(propsChildB.pods.data); - expect(propsChildA.pods.data[0]).toBe(propsChildB.pods.data[0]); - expect(propsChildA.resources.pods).toBe(propsChildB.resources.pods); - expect(propsChildA.resources.pods.data).toBe(propsChildB.resources.pods.data); - expect(propsChildA.resources.pods.data[0]).toBe(propsChildB.resources.pods.data[0]); - - // pod 'resource' object (with data, loaded, etc.) object - expect(propsChildA.pod).toBe(propsChildB.pod); - expect(propsChildA.pod.data).toBe(propsChildB.pod.data); - expect(propsChildA.resources.pod).toBe(propsChildB.resources.pod); - expect(propsChildA.resources.pod.data).toBe(propsChildB.resources.pod.data); - }); - - it('should fetch data just once when two Firehose components requests the same data', async () => { - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: true, - namespace: 'my-namespace', - }, - { - prop: 'pod', - kind: 'Pod', - namespace: 'my-namespace', - name: 'my-pod', - }, - ]; - render( - - - - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - // Assert that API calls are just triggered once - expect(k8sListMock).toHaveBeenCalledTimes(1); - expect(k8sListMock.mock.calls[0]).toEqual([ - PodModel, - { limit: 250, ns: 'my-namespace' }, - true, - {}, - ]); - k8sListMock.mockClear(); - expect(k8sGetMock).toHaveBeenCalledTimes(1); - expect(k8sGetMock.mock.calls[0]).toEqual([PodModel, 'my-pod', 'my-namespace', {}, {}]); - k8sGetMock.mockClear(); - - // Expect initial render child-props - const podsNotLoadedYet = { - kind: 'Pod', - data: [], - loaded: false, - loadError: '', - filters: {}, - selected: null, - optional: undefined, - }; - const podNotLoadedYet = { - data: {}, - loaded: false, - loadError: '', - optional: undefined, - }; - const podsNotLoadedYetProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: [ - 'core~v1~Pod---{"ns":"my-namespace"}', - 'core~v1~Pod---{"ns":"my-namespace","name":"my-pod"}', - ], - loaded: false, - // Yes, same data twice at the moment. - pods: podsNotLoadedYet, - pod: podNotLoadedYet, - resources: { pods: podsNotLoadedYet, pod: podNotLoadedYet }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(2); - expect(resourceUpdate.mock.calls[0][0]).toEqual(podsNotLoadedYetProps); - expect(resourceUpdate.mock.calls[1][0]).toEqual(podsNotLoadedYetProps); - - // Finish API call - await act(async () => { - jest.runAllTimers(); - }); - - // Expect updated child-props - const podsLoaded = { - kind: 'Pod', - data: ['my-pod1', 'my-pod2', 'my-pod3'].map((name) => ({ - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name, - namespace: 'my-namespace', - resourceVersion: '123', - }, - })), - loaded: true, - loadError: '', - filters: {}, - selected: null, - optional: undefined, - }; - const podLoaded = { - data: { - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name: 'my-pod', - namespace: 'my-namespace', - resourceVersion: '123', - }, - }, - loaded: true, - loadError: '', - optional: undefined, - }; - const podsLoadedProps = { - ...firehoseChildPropsWithoutModels, - k8sModels: ImmutableMap({ Pod: PodModel }), - reduxIDs: [ - 'core~v1~Pod---{"ns":"my-namespace"}', - 'core~v1~Pod---{"ns":"my-namespace","name":"my-pod"}', - ], - loaded: true, - // Yes, same data twice at the moment. - pods: podsLoaded, - pod: podLoaded, - resources: { pods: podsLoaded, pod: podLoaded }, - }; - expect(resourceUpdate).toHaveBeenCalledTimes(6); - // skip rerendering 2 so that both data sets are loaded - expect(resourceUpdate.mock.calls[4][0]).toEqual(podsLoadedProps); - expect(resourceUpdate.mock.calls[5][0]).toEqual(podsLoadedProps); - const propsChildA = resourceUpdate.mock.calls[4][0]; - const propsChildB = resourceUpdate.mock.calls[5][0]; - resourceUpdate.mockClear(); - - // Check that all data shares the same identity for the loaded data. - expect(propsChildA).not.toEqual(propsChildB); // Compared values have no visual difference, but should be equal, or? - expect(propsChildA).not.toBe(propsChildB); // Compared values have no visual difference, but should be the same, or? - - // pods 'resource' object (with data, loaded, etc.) object - expect(propsChildA.pods).toEqual(propsChildB.pods); - expect(propsChildA.pods).not.toBe(propsChildB.pods); // Could be the same? - expect(propsChildA.pods.data).toBe(propsChildB.pods.data); - expect(propsChildA.pods.data[0]).toBe(propsChildB.pods.data[0]); - - expect(propsChildA.resources.pods).toEqual(propsChildB.resources.pods); - expect(propsChildA.resources.pods).not.toBe(propsChildB.resources.pods); // Could be the same? - expect(propsChildA.resources.pods.data).toBe(propsChildB.resources.pods.data); - expect(propsChildA.resources.pods.data[0]).toBe(propsChildB.resources.pods.data[0]); - - // pod 'resource' object (with data, loaded, etc.) object - expect(propsChildA.pod).not.toBe(propsChildB.pod); // Could be the same? - expect(propsChildA.data).toBe(propsChildB.data); - expect(propsChildA.resources.pod).not.toBe(propsChildB.resources.pod); // Could be the same? - expect(propsChildA.resources.pod.data).toBe(propsChildB.resources.pod.data); - }); -}); - -describe('Firehose together with useK8sWatchResources', () => { - // Objects under test - const firehoseUpdate = jest.fn(); - const Child: FC = (props) => { - firehoseUpdate(props); - return null; - }; - - const resourcesUpdate = jest.fn(); - const WatchResources: FC<{ initResources: WatchK8sResources<{}> }> = ({ initResources }) => { - resourcesUpdate(useK8sWatchResources(initResources)); - return null; - }; - - beforeEach(() => { - // Init k8s redux store with just one model - store = createStore(combineReducers(SDKReducers), {}, applyMiddleware(thunk)); - store.dispatch( - receivedResources({ - models: [PodModel], - adminResources: [], - allResources: [], - configResources: [], - clusterOperatorConfigResources: [], - namespacedSet: null, - safeResources: [], - groupVersionMap: {}, - }), - ); - - jest.useFakeTimers({ legacyFakeTimers: true }); - jest.resetAllMocks(); - - k8sListMock.mockReturnValue(Promise.resolve(podList)); - k8sGetMock.mockReturnValue(Promise.resolve(podData)); - const wsMock = { - onclose: () => wsMock, - ondestroy: () => wsMock, - onbulkmessage: () => wsMock, - destroy: () => wsMock, - }; - k8sWatchMock.mockReturnValue(wsMock); - }); - - afterEach(async () => { - // Ensure that there is no timer left which triggers a rerendering - await act(async () => { - jest.runAllTimers(); - }); - - cleanup(); - - // Ensure that there is no unexpected api calls - expect(k8sListMock).toHaveBeenCalledTimes(0); - expect(k8sGetMock).toHaveBeenCalledTimes(0); - expect(k8sWatchMock).toHaveBeenCalledTimes(0); - expect(firehoseUpdate).toHaveBeenCalledTimes(0); - expect(resourcesUpdate).toHaveBeenCalledTimes(0); - - jest.clearAllTimers(); - jest.useRealTimers(); - }); - - it('should fetch data just once and return the same data for both (Firehose first)', async () => { - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: true, - namespace: 'my-namespace', - }, - { - prop: 'pod', - kind: 'Pod', - namespace: 'my-namespace', - name: 'my-pod', - }, - ]; - const initResources: WatchK8sResources<{}> = { - pods: { - kind: 'Pod', - namespace: 'my-namespace', - isList: true, - }, - pod: { - kind: 'Pod', - namespace: 'my-namespace', - name: 'my-pod', - }, - }; - - render( - - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - // Finish API calls - await act(async () => { - jest.runAllTimers(); - }); - - // Assert that API calls are just triggered once - expect(k8sListMock).toHaveBeenCalledTimes(1); - expect(k8sListMock.mock.calls[0]).toEqual([ - PodModel, - { limit: 250, ns: 'my-namespace' }, - true, - {}, - ]); - k8sListMock.mockClear(); - - expect(k8sGetMock).toHaveBeenCalledTimes(1); - expect(k8sGetMock.mock.calls[0]).toEqual([PodModel, 'my-pod', 'my-namespace', {}, {}]); - k8sGetMock.mockClear(); - - // Components was rendered the right amount of time (loaded: false, loaded: true) - expect(firehoseUpdate).toHaveBeenCalledTimes(3); - expect(resourcesUpdate).toHaveBeenCalledTimes(3); - const lastFirehoseChildProps = firehoseUpdate.mock.calls[2][0]; - const lastUseResourcesHookResult = resourcesUpdate.mock.calls[2][0]; - firehoseUpdate.mockClear(); - resourcesUpdate.mockClear(); - - // Tests earlier checks the exact format, we focus here on comparing the data instances - expect(lastFirehoseChildProps.pods).toBeTruthy(); - expect(lastFirehoseChildProps.pod).toBeTruthy(); - expect(lastUseResourcesHookResult.pods).toBeTruthy(); - expect(lastUseResourcesHookResult.pod).toBeTruthy(); - - // Result objects looks different for list (not a requirement, but the status quo) - expect(lastFirehoseChildProps.pods).not.toEqual(lastUseResourcesHookResult.pods); - // but is the same for single items at the moment (also not a requirement, but the status quo) - expect(lastFirehoseChildProps.pod).toEqual(lastUseResourcesHookResult.pod); - expect(lastFirehoseChildProps.pod).not.toBe(lastUseResourcesHookResult.pod); - - // The data should be the same! - expect(lastFirehoseChildProps.pods.data).toEqual(lastUseResourcesHookResult.pods.data); - expect(lastFirehoseChildProps.pod.data).toEqual(lastUseResourcesHookResult.pod.data); - - // And they also should return the same instance for lists - expect(lastFirehoseChildProps.pods.data).toBe(lastUseResourcesHookResult.pods.data); - expect(lastFirehoseChildProps.pods.data[0]).toBe(lastUseResourcesHookResult.pods.data[0]); - expect(lastFirehoseChildProps.pods.data[1]).toBe(lastUseResourcesHookResult.pods.data[1]); - expect(lastFirehoseChildProps.pods.data[2]).toBe(lastUseResourcesHookResult.pods.data[2]); - - // And they also should return the same instance for single items - expect(lastFirehoseChildProps.pod.data).not.toBe(lastUseResourcesHookResult.pod.data); // Should be the same, or? - }); - - it('should fetch data just once and return the same data for both (useK8sWatchResources first)', async () => { - const initResources: WatchK8sResources<{}> = { - pods: { - kind: 'Pod', - namespace: 'my-namespace', - isList: true, - }, - pod: { - kind: 'Pod', - namespace: 'my-namespace', - name: 'my-pod', - }, - }; - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: true, - namespace: 'my-namespace', - }, - { - prop: 'pod', - kind: 'Pod', - namespace: 'my-namespace', - name: 'my-pod', - }, - ]; - - render( - - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - // Finish API calls - await act(async () => { - jest.runAllTimers(); - }); - - // Assert that API calls are just triggered once - expect(k8sListMock).toHaveBeenCalledTimes(1); - expect(k8sListMock.mock.calls[0]).toEqual([ - PodModel, - { limit: 250, ns: 'my-namespace' }, - true, - {}, - ]); - k8sListMock.mockClear(); - - expect(k8sGetMock).toHaveBeenCalledTimes(1); - expect(k8sGetMock.mock.calls[0]).toEqual([PodModel, 'my-pod', 'my-namespace', {}, {}]); - k8sGetMock.mockClear(); - - // Components was rendered the right amount of time (loaded: false, loaded: true) - expect(firehoseUpdate).toHaveBeenCalledTimes(3); - expect(resourcesUpdate).toHaveBeenCalledTimes(4); - const lastFirehoseChildProps = firehoseUpdate.mock.calls[2][0]; - const lastUseResourcesHookResult = resourcesUpdate.mock.calls[3][0]; - firehoseUpdate.mockClear(); - resourcesUpdate.mockClear(); - - // Tests earlier checks the exact format, we focus here on comparing the data instances - expect(lastFirehoseChildProps.pods).toBeTruthy(); - expect(lastFirehoseChildProps.pod).toBeTruthy(); - expect(lastUseResourcesHookResult.pods).toBeTruthy(); - expect(lastUseResourcesHookResult.pod).toBeTruthy(); - - // Result objects looks different for list (not a requirement, but the status quo) - expect(lastFirehoseChildProps.pods).not.toEqual(lastUseResourcesHookResult.pods); - // but is the same for single items at the moment (also not a requirement, but the status quo) - expect(lastFirehoseChildProps.pod).toEqual(lastUseResourcesHookResult.pod); - expect(lastFirehoseChildProps.pod).not.toBe(lastUseResourcesHookResult.pod); - - // The data should be the same! - expect(lastFirehoseChildProps.pods.data).toEqual(lastUseResourcesHookResult.pods.data); - expect(lastFirehoseChildProps.pod.data).toEqual(lastUseResourcesHookResult.pod.data); - - // And they also should return the same instance for lists - expect(lastFirehoseChildProps.pods.data).toBe(lastUseResourcesHookResult.pods.data); - expect(lastFirehoseChildProps.pods.data[0]).toBe(lastUseResourcesHookResult.pods.data[0]); - expect(lastFirehoseChildProps.pods.data[1]).toBe(lastUseResourcesHookResult.pods.data[1]); - expect(lastFirehoseChildProps.pods.data[2]).toBe(lastUseResourcesHookResult.pods.data[2]); - - // And they also should return the same instance for single items - expect(lastFirehoseChildProps.pod.data).not.toBe(lastUseResourcesHookResult.pod.data); // Should be the same, or? - }); - - // Regression test for "Git import page crashes after load" on 4.9 - // https://bugzilla.redhat.com/show_bug.cgi?id=2069621 - describe('regression test for bug #2069621', () => { - // This reproduce the original issue - it('should return an array for Firehose isList=true even when useK8sWatchResources isList=false is called without a name (Firehose first)', async () => { - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: true, - namespace: 'my-namespace', - }, - ]; - const initResources: WatchK8sResources<{}> = { - pods: { - kind: 'Pod', - namespace: 'my-namespace', - name: '', // Should not be supported by the API, but this happens sometimes - isList: false, - optional: true, - }, - }; - - render( - - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - // Finish API calls - await act(async () => { - jest.runAllTimers(); - }); - - // Assert that API calls are just triggered once - expect(k8sListMock).toHaveBeenCalledTimes(1); - expect(k8sListMock.mock.calls[0]).toEqual([ - PodModel, - { limit: 250, ns: 'my-namespace' }, - true, - {}, - ]); - k8sListMock.mockClear(); - - // Components was rendered the right amount of time (loaded: false, loaded: true) - expect(firehoseUpdate).toHaveBeenCalledTimes(2); - const lastFirehoseChildProps = firehoseUpdate.mock.calls[1][0]; - firehoseUpdate.mockClear(); - - expect(resourcesUpdate).toHaveBeenCalledTimes(2); - const lastUseResourcesHookResult = resourcesUpdate.mock.calls[1][0]; - resourcesUpdate.mockClear(); - - // But the Firehose call defines isList correctly and should still work - // and should return an array. - expect(lastFirehoseChildProps.pods).toEqual({ - kind: 'Pod', - data: ['my-pod1', 'my-pod2', 'my-pod3'].map((name) => ({ - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name, - namespace: 'my-namespace', - resourceVersion: '123', - }, - })), - loaded: true, - loadError: '', - filters: {}, - selected: null, - optional: undefined, - }); - - // The hook should not return any data because the name is missing! - // Instead it returns the internal redux state of the list above as object. - expect(lastUseResourcesHookResult.pods).toEqual({ - loaded: true, - loadError: '', - data: { - '(my-namespace)-my-pod1': { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod1', namespace: 'my-namespace', resourceVersion: '123' }, - }, - '(my-namespace)-my-pod2': { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod2', namespace: 'my-namespace', resourceVersion: '123' }, - }, - '(my-namespace)-my-pod3': { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod3', namespace: 'my-namespace', resourceVersion: '123' }, - }, - }, - }); - }); - - // And this 3 cases tests against other call orders / isList=true/false combinations... - it('should return an array for Firehose isList=true even when useK8sWatchResources isList=false is called without a name (useK8sWatchResources first)', async () => { - const initResources: WatchK8sResources<{}> = { - pods: { - kind: 'Pod', - namespace: 'my-namespace', - name: '', // Should not be supported by the API, but this happens sometimes - isList: false, - optional: true, - }, - }; - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: true, - namespace: 'my-namespace', - }, - ]; - - render( - - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - // Finish API calls - await act(async () => { - jest.runAllTimers(); - }); - - // Assert that API calls are just triggered once - expect(k8sListMock).toHaveBeenCalledTimes(1); - expect(k8sListMock.mock.calls[0]).toEqual([ - PodModel, - { limit: 250, ns: 'my-namespace' }, - true, - {}, - ]); - k8sListMock.mockClear(); - - // Components was rendered the right amount of time (loaded: false, loaded: true) - expect(resourcesUpdate).toHaveBeenCalledTimes(3); - const lastUseResourcesHookResult = resourcesUpdate.mock.calls[2][0]; - resourcesUpdate.mockClear(); - - expect(firehoseUpdate).toHaveBeenCalledTimes(2); - const lastFirehoseChildProps = firehoseUpdate.mock.calls[1][0]; - firehoseUpdate.mockClear(); - - // The hook could not return any data because the name is missing. - expect(lastUseResourcesHookResult.pods).toEqual({ - loaded: true, - loadError: '', - data: { - '(my-namespace)-my-pod1': { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod1', namespace: 'my-namespace', resourceVersion: '123' }, - }, - '(my-namespace)-my-pod2': { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod2', namespace: 'my-namespace', resourceVersion: '123' }, - }, - '(my-namespace)-my-pod3': { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod3', namespace: 'my-namespace', resourceVersion: '123' }, - }, - }, - }); - - // But the Firehose call defines isList correctly and should still work - // and should return an array. - expect(lastFirehoseChildProps.pods).toEqual({ - kind: 'Pod', - data: ['my-pod1', 'my-pod2', 'my-pod3'].map((name) => ({ - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name, - namespace: 'my-namespace', - resourceVersion: '123', - }, - })), - loaded: true, - loadError: '', - filters: {}, - selected: null, - optional: undefined, - }); - }); - - it('should return an array for useK8sWatchResources isList=true even when Firehose isList=false is called without a name (Firehose first)', async () => { - // Without a name the k8sGet API is called, but it returns a list anyway. - k8sGetMock.mockReturnValue(Promise.resolve(podList)); - - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: false, - namespace: 'my-namespace', - name: '', - }, - ]; - const initResources: WatchK8sResources<{}> = { - pods: { - kind: 'Pod', - namespace: 'my-namespace', - isList: true, - }, - }; - - render( - - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - // Finish API calls - await act(async () => { - jest.runAllTimers(); - }); - - // Assert that API calls are just triggered once - expect(k8sGetMock).toHaveBeenCalledTimes(1); - expect(k8sGetMock.mock.calls[0]).toEqual([ - PodModel, - '', // Without a name above this calls the get api, but it still returns a list. - 'my-namespace', - {}, - {}, - ]); - k8sGetMock.mockClear(); - - // Components was rendered the right amount of time (loaded: false, loaded: true) - expect(firehoseUpdate).toHaveBeenCalledTimes(2); - const lastFirehoseChildProps = firehoseUpdate.mock.calls[1][0]; - firehoseUpdate.mockClear(); - - expect(resourcesUpdate).toHaveBeenCalledTimes(2); - const lastUseResourcesHookResult = resourcesUpdate.mock.calls[1][0]; - resourcesUpdate.mockClear(); - - // The Firehose call defines isList=false, so it returns the full API response. - expect(lastFirehoseChildProps.pods).toEqual({ - data: { - apiVersion: 'v1', - items: [ - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod1', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod2', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod3', namespace: 'my-namespace', resourceVersion: '123' }, - }, - ], - kind: 'PodList', - metadata: { resourceVersion: '123' }, - }, - loaded: true, - loadError: '', - optional: undefined, - }); - - // But the hook defines isList=true and converts it automatically to an array. - // At the moment it doesn't extract the 'values' key. - expect(lastUseResourcesHookResult.pods).toEqual({ - loaded: true, - loadError: '', - data: [ - 'v1', - 'PodList', - [ - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod1', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod2', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod3', namespace: 'my-namespace', resourceVersion: '123' }, - }, - ], - { resourceVersion: '123' }, - ], - }); - }); - - it('should return an array for useK8sWatchResources isList=true even when Firehose isList=false is called without a name (useK8sWatchResources first)', async () => { - // Without a name the k8sGet API is called, but it returns a list anyway. - k8sGetMock.mockReturnValue(Promise.resolve(podList)); - - const initResources: WatchK8sResources<{}> = { - pods: { - kind: 'Pod', - namespace: 'my-namespace', - isList: true, - optional: true, - }, - }; - const resources = [ - { - prop: 'pods', - kind: 'Pod', - isList: false, - namespace: 'my-namespace', - name: '', - }, - ]; - - render( - - - - - - , - { legacyRoot: true }, // TODO(react18): Remove Firehose before using ReactDOM.createRoot - ); - - // Finish API calls - await act(async () => { - jest.runAllTimers(); - }); - - expect(k8sGetMock).toHaveBeenCalledTimes(1); - expect(k8sGetMock.mock.calls[0]).toEqual([ - PodModel, - '', // Without a name above this calls the get api, but it still returns a list. - 'my-namespace', - {}, - {}, - ]); - k8sGetMock.mockClear(); - - // Components was rendered the right amount of time (loaded: false, loaded: true) - expect(resourcesUpdate).toHaveBeenCalledTimes(3); - const lastUseResourcesHookResult = resourcesUpdate.mock.calls[2][0]; - resourcesUpdate.mockClear(); - - expect(firehoseUpdate).toHaveBeenCalledTimes(2); - const lastFirehoseChildProps = firehoseUpdate.mock.calls[1][0]; - firehoseUpdate.mockClear(); - - // The hook could not return any data because the name is missing. - expect(lastUseResourcesHookResult.pods).toEqual({ - data: [ - 'v1', - 'PodList', - [ - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod1', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod2', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod3', namespace: 'my-namespace', resourceVersion: '123' }, - }, - ], - { resourceVersion: '123' }, - ], - loadError: '', - loaded: true, - }); - - // But Firehose can return a pod when call defines isList correctly and should still work - // and should get an array. - expect(lastFirehoseChildProps.pods).toEqual({ - data: { - apiVersion: 'v1', - items: [ - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod1', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod2', namespace: 'my-namespace', resourceVersion: '123' }, - }, - { - apiVersion: 'v1', - kind: 'Pod', - metadata: { name: 'my-pod3', namespace: 'my-namespace', resourceVersion: '123' }, - }, - ], - kind: 'PodList', - metadata: { resourceVersion: '123' }, - }, - loaded: true, - loadError: '', - optional: undefined, - }); - }); - }); -}); diff --git a/frontend/public/components/utils/firehose.jsx b/frontend/public/components/utils/firehose.jsx deleted file mode 100644 index 8b0bb7eb3e3..00000000000 --- a/frontend/public/components/utils/firehose.jsx +++ /dev/null @@ -1,312 +0,0 @@ -/* eslint-disable tsdoc/syntax */ -import * as _ from 'lodash'; -import { memo, Component } from 'react'; -import * as PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { Map as ImmutableMap } from 'immutable'; - -import { inject } from './inject'; -import { makeReduxID, makeQuery } from './k8s-watcher'; -import * as k8sActions from '../../actions/k8s'; - -import { - INTERNAL_REDUX_IMMUTABLE_TOARRAY_CACHE_SYMBOL, - INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL, -} from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher'; -import { getK8sModel } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sModel'; - -const shallowMapEquals = (a, b) => { - if (a === b || (a.size === 0 && b.size === 0)) { - return true; - } - if (a.size !== b.size) { - return false; - } - return a.every((v, k) => b.get(k) === v); -}; - -export const processReduxId = ({ k8s }, props) => { - const { reduxID, isList, filters } = props; - - if (!reduxID) { - return {}; - } - - if (!isList) { - let stuff = k8s.get(reduxID); - if (!stuff) { - return {}; - } - if (!stuff[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL]) { - stuff[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL] = stuff.toJSON(); - } - stuff = stuff[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL]; - return { ...stuff, optional: props.optional }; - } - - let data = k8s.getIn([reduxID, 'data']); - const _filters = k8s.getIn([reduxID, 'filters']); - const selected = k8s.getIn([reduxID, 'selected']); - - if (data && data.toArray) { - if (!data[INTERNAL_REDUX_IMMUTABLE_TOARRAY_CACHE_SYMBOL]) { - data[INTERNAL_REDUX_IMMUTABLE_TOARRAY_CACHE_SYMBOL] = data.toArray().map((a) => { - if (a.toJSON) { - if (!a[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL]) { - a[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL] = a.toJSON(); - } - return a[INTERNAL_REDUX_IMMUTABLE_TOJSON_CACHE_SYMBOL]; - } - return a; - }); - } - data = data[INTERNAL_REDUX_IMMUTABLE_TOARRAY_CACHE_SYMBOL]; - } - - return { - data, - // This is a hack to allow filters passed down from props to make it to - // the injected component. Ideally filters should all come from redux. - filters: _.extend({}, _filters && _filters.toJS(), filters), - kind: props.kind, - loadError: k8s.getIn([reduxID, 'loadError']), - loaded: k8s.getIn([reduxID, 'loaded']), - optional: props.optional, - selected, - }; -}; - -const worstError = (errors) => { - let worst = errors && errors[0]; - for (const e of errors) { - if (e.status === 403) { - return e; - } - if (e.status === 401) { - worst = e; - continue; - } - if (worst.status === 401) { - continue; - } - if (e.status > worst.status) { - worst = e; - continue; - } - } - return worst; -}; - -const mapStateToProps = ({ k8s }) => ({ - k8s, -}); - -const propsAreEqual = (prevProps, nextProps) => { - if (nextProps.children === prevProps.children && nextProps.reduxes === prevProps.reduxes) { - return nextProps.reduxes.every( - ({ reduxID }) => prevProps.k8s.get(reduxID) === nextProps.k8s.get(reduxID), - ); - } - return false; -}; - -// A wrapper Component that takes data out of redux for a list or object at some reduxID ... -// passing it to children -const ConnectToState = connect(mapStateToProps)( - memo(({ k8s, reduxes, children }) => { - const resources = {}; - - reduxes.forEach((redux) => { - resources[redux.prop] = processReduxId({ k8s }, redux); - }); - - const required = _.filter(resources, (r) => !r.optional); - const loaded = _.every(resources, (resource) => - resource.optional ? resource.loaded || !_.isEmpty(resource.loadError) : resource.loaded, - ); - const loadError = worstError(_.map(required, 'loadError').filter(Boolean)); - - const k8sResults = Object.assign({}, resources, { - filters: Object.assign({}, ..._.map(resources, 'filters')), - loaded, - loadError, - reduxIDs: _.map(reduxes, 'reduxID'), - resources, - }); - - return inject(children, k8sResults); - }, propsAreEqual), -); - -const stateToProps = (state, { resources }) => { - const { k8s } = state; - const k8sModels = resources.reduce( - (models, { kind }) => models.set(kind, getK8sModel(k8s, kind)), - ImmutableMap(), - ); - const loaded = (r) => - r.optional || - k8s.getIn([ - makeReduxID( - k8sModels.get(r.kind), - makeQuery(r.namespace, r.selector, r.fieldSelector, r.name), - ), - 'loaded', - ]); - - return { - k8sModels, - loaded: resources.every(loaded), - inFlight: k8s.getIn(['RESOURCES', 'inFlight']), - }; -}; - -export const Firehose = connect( - stateToProps, - { - stopK8sWatch: k8sActions.stopK8sWatch, - watchK8sObject: k8sActions.watchK8sObject, - watchK8sList: k8sActions.watchK8sList, - }, - null, - { - areStatesEqual: (next, prev) => next.k8s === prev.k8s, - areStatePropsEqual: (next, prev) => - next.loaded === prev.loaded && - next.inFlight === prev.inFlight && - shallowMapEquals(next.k8sModels, prev.k8sModels), - }, -)( - /** @augments {React.Component, doNotConnectToState?: boolean}>> */ - class Firehose extends Component { - state = { - firehoses: [], - }; - - // TODO: Convert this to `componentDidMount` - // eslint-disable-next-line camelcase - UNSAFE_componentWillMount() { - this.start(); - } - - componentWillUnmount() { - this.clear(); - } - - shouldComponentUpdate(nextProps, nextState) { - if ( - Object.keys(nextProps).length === Object.keys(this.props).length && - Object.keys(nextProps) - .filter((key) => key !== 'inFlight') - .every((key) => nextProps[key] === this.props[key]) && - (nextState === this.state || - (nextState.firehoses.length === 0 && this.state.firehoses.length === 0)) - ) { - return this.props.loaded ? false : this.props.inFlight !== nextProps.inFlight; - } - return true; - } - - componentDidUpdate(prevProps) { - const discoveryComplete = - !this.props.inFlight && !this.props.loaded && this.state.firehoses.length === 0; - const resourcesChanged = - prevProps.resources !== this.props.resources && - _.intersectionWith(prevProps.resources, this.props.resources, _.isEqual).length !== - this.props.resources.length; - - if (discoveryComplete || resourcesChanged) { - this.clear(); - this.start(); - } - } - - start() { - const { watchK8sList, watchK8sObject, resources, k8sModels, inFlight } = this.props; - - let firehoses = []; - if (!(inFlight && _.some(resources, ({ kind }) => !k8sModels.get(kind)))) { - firehoses = resources - .map((resource) => { - const query = makeQuery( - resource.namespace, - resource.selector, - resource.fieldSelector, - resource.name, - resource.limit, - ); - const k8sKind = k8sModels.get(resource.kind); - const id = makeReduxID(k8sKind, query); - return _.extend({}, resource, { query, id, k8sKind }); - }) - .filter((f) => { - if (_.isEmpty(f.k8sKind)) { - // eslint-disable-next-line no-console - console.warn(`No model registered for ${f.kind}`); - } - return !_.isEmpty(f.k8sKind); - }); - } - - firehoses.forEach(({ id, query, k8sKind, isList, name, namespace, partialMetadata }) => - isList - ? watchK8sList(id, query, k8sKind, null, partialMetadata) - : watchK8sObject(id, name, namespace, query, k8sKind, partialMetadata), - ); - this.setState({ firehoses }); - } - - clear() { - this.state.firehoses.forEach(({ id }) => this.props.stopK8sWatch(id)); - } - - render() { - if (this.props.loaded || this.state.firehoses.length > 0) { - const children = inject(this.props.children, _.omit(this.props, ['children', 'resources'])); - - if (this.props.doNotConnectToState) { - return children; - } - - const reduxes = this.state.firehoses.map( - ({ id, prop, isList, filters, optional, kind }) => ({ - reduxID: id, - prop, - isList, - filters, - optional, - kind, - }), - ); - return {children}; - } - return null; - } - }, -); -Firehose.WrappedComponent.contextTypes = { - router: PropTypes.object, -}; - -Firehose.contextTypes = { - store: PropTypes.object, -}; - -Firehose.propTypes = { - children: PropTypes.node, - expand: PropTypes.bool, - doNotConnectToState: PropTypes.bool, - resources: PropTypes.arrayOf( - PropTypes.shape({ - kind: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, - name: PropTypes.string, - namespace: PropTypes.string, - selector: PropTypes.object, - fieldSelector: PropTypes.string, - isList: PropTypes.bool, - optional: PropTypes.bool, // do not block children-rendering while resource is still being loaded; do not fail if resource is missing (404) - limit: PropTypes.number, - partialMetadata: PropTypes.bool, - }), - ).isRequired, -}; diff --git a/frontend/public/components/utils/headings.tsx b/frontend/public/components/utils/headings.tsx index a608ae709fb..1d670c432f5 100644 --- a/frontend/public/components/utils/headings.tsx +++ b/frontend/public/components/utils/headings.tsx @@ -24,7 +24,7 @@ import { K8sResourceKindReference, referenceForExtensionModel, } from '../../module/k8s'; -import type { FirehoseResult } from './types'; +import type { WatchK8sResultsObject } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; import { ResourceIcon } from './resource-icon'; import { ManagedByOperatorLink } from './managed-by'; import { Action } from '@console/dynamic-plugin-sdk/src/lib-core'; @@ -250,7 +250,7 @@ export type ConnectedPageHeadingProps = Omit kind?: K8sResourceKindReference; kindObj?: K8sKind; menuActions?: Function[] | KebabOptionsCreator; // FIXME should be "KebabAction[] |" refactor pipeline-actions.tsx, etc. - obj?: FirehoseResult; + obj?: WatchK8sResultsObject; /** A component to override the title of the page */ OverrideTitle?: ComponentType<{ obj?: K8sResourceKind }>; resourceKeys?: string[]; diff --git a/frontend/public/components/utils/index.tsx b/frontend/public/components/utils/index.tsx index 4f049f3039b..d379bb119a3 100644 --- a/frontend/public/components/utils/index.tsx +++ b/frontend/public/components/utils/index.tsx @@ -9,7 +9,6 @@ export * from './resource-log'; export * from './horizontal-nav'; export * from './details-page'; export * from './inject'; -export * from './firehose'; export * from './status-box'; export * from './headings'; export * from './units'; diff --git a/frontend/public/components/utils/list-dropdown.tsx b/frontend/public/components/utils/list-dropdown.tsx index 94d34fd68d6..416f6ef0f70 100644 --- a/frontend/public/components/utils/list-dropdown.tsx +++ b/frontend/public/components/utils/list-dropdown.tsx @@ -15,7 +15,6 @@ import { useTranslation } from 'react-i18next'; import { useCreateNamespaceModal } from '@console/shared/src/hooks/useCreateNamespaceModal'; import { useCreateProjectModal } from '@console/shared/src/hooks/useCreateProjectModal'; import { - FirehoseResource, K8sResourceCommon, K8sModel, K8sResourceKind, @@ -26,7 +25,7 @@ const getKey = (key, keyKind) => { return keyKind ? `${key}-${keyKind}` : key; }; -interface ListDropdownResource extends Partial { +interface ListDropdownResource extends Partial { data?: K8sResourceCommon[]; } @@ -207,7 +206,7 @@ export const ListDropdown: FC = (props) => { return {}; } return props.resources.reduce((acc, resource) => { - // Use prop as key if provided, otherwise fallback to kind (matches original Firehose behavior) + // Use prop as key if provided, otherwise fallback to kind const key = resource.prop || resource.kind; acc[key] = { kind: resource.kind, diff --git a/frontend/public/components/utils/types.ts b/frontend/public/components/utils/types.ts index b677106ce47..6ebb24da200 100644 --- a/frontend/public/components/utils/types.ts +++ b/frontend/public/components/utils/types.ts @@ -1,26 +1,13 @@ -import type { - K8sResourceKindReference, - K8sResourceCommon, -} from '@console/dynamic-plugin-sdk/src/extensions/console-types'; -import type { Selector } from '@console/dynamic-plugin-sdk/src/api/common-types'; -import type { K8sResourceKind } from '../../module/k8s/types'; +import type { WatchK8sResource } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; -export type FirehoseResult< - R extends K8sResourceCommon | K8sResourceCommon[] = K8sResourceKind[] -> = { - loaded: boolean; - loadError: string; - optional?: boolean; - data: R; - kind?: string; -}; - -export type FirehoseResultObject = { [key: string]: K8sResourceCommon | K8sResourceCommon[] }; - -export type FirehoseResourcesResult< - R extends FirehoseResultObject = { [key: string]: K8sResourceCommon | K8sResourceCommon[] } -> = { - [k in keyof R]: FirehoseResult; +/** + * Extension of WatchK8sResource that includes the prop field required by + * legacy components like MultiListPage and DetailsPage. + * Use WatchK8sResource from @console/dynamic-plugin-sdk for new components + * that use the useK8sWatchResources hook. + */ +export type WatchK8sResourceWithProp = WatchK8sResource & { + prop: string; }; /* @@ -47,19 +34,6 @@ export const enum EnvType { ENV_FROM = 1, } -export type FirehoseResource = { - kind: K8sResourceKindReference; - name?: string; - namespace?: string; - isList?: boolean; - selector?: Selector; - prop: string; - namespaced?: boolean; - optional?: boolean; - limit?: number; - fieldSelector?: string; -}; - export type HumanizeResult = { string: string; value: number; diff --git a/frontend/public/locales/en/public.json b/frontend/public/locales/en/public.json index dfb0a4d277e..78e71d7e755 100644 --- a/frontend/public/locales/en/public.json +++ b/frontend/public/locales/en/public.json @@ -1738,6 +1738,21 @@ "Exec command": "Exec command", "HTTP GET": "HTTP GET", "TCP socket (port)": "TCP socket (port)", + "Critical": "Critical", + "Loading {{title}} status": "Loading {{title}} status", + "Waiting for the build": "Waiting for the build", + "graph timespan": "graph timespan", + "Reset zoom": "Reset zoom", + "Displaying with reduced resolution due to large dataset.": "Displaying with reduced resolution due to large dataset.", + "query browser chart": "query browser chart", + "Ungraphable results": "Ungraphable results", + "Query results include range vectors, which cannot be graphed. Try adding a function to transform the data.": "Query results include range vectors, which cannot be graphed. Try adding a function to transform the data.", + "Query result is a string, which cannot be graphed.": "Query result is a string, which cannot be graphed.", + "The resulting dataset is too large to graph.": "The resulting dataset is too large to graph.", + "Stacked": "Stacked", + "unknown host": "unknown host", + "Unknown user": "Unknown user", + "Just now": "Just now", "Delete {{label}} subject?": "Delete {{label}} subject?", "Are you sure you want to delete subject {{name}} of type {{kind}}?": "Are you sure you want to delete subject {{name}} of type {{kind}}?", "Impersonate {{kind}} \"{{name}}\"": "Impersonate {{kind}} \"{{name}}\"", @@ -1785,22 +1800,6 @@ "{{pluginName}} might have violated the Console Content Security Policy. Refer to the browser's console logs for details.": "{{pluginName}} might have violated the Console Content Security Policy. Refer to the browser's console logs for details.", "prometheusBaseURL not set": "prometheusBaseURL not set", "alertManagerBaseURL not set": "alertManagerBaseURL not set", - "Critical": "Critical", - "Loading {{title}} status": "Loading {{title}} status", - "Waiting for the build": "Waiting for the build", - "graph timespan": "graph timespan", - "Reset zoom": "Reset zoom", - "Displaying with reduced resolution due to large dataset.": "Displaying with reduced resolution due to large dataset.", - "query browser chart": "query browser chart", - "Ungraphable results": "Ungraphable results", - "Query results include range vectors, which cannot be graphed. Try adding a function to transform the data.": "Query results include range vectors, which cannot be graphed. Try adding a function to transform the data.", - "Query result is a string, which cannot be graphed.": "Query result is a string, which cannot be graphed.", - "The resulting dataset is too large to graph.": "The resulting dataset is too large to graph.", - "Stacked": "Stacked", - "unknown host": "unknown host", - "Unknown user": "Unknown user", - "Just now": "Just now", - "Select service account": "Select service account", "Delete {{kind}}": "Delete {{kind}}", "Disable": "Disable", "Disabled": "Disabled",