From 05381518921c8aab93b2a3888b0a0ad0cdb36446 Mon Sep 17 00:00:00 2001 From: cyril-ui-developer Date: Thu, 15 Jan 2026 15:57:44 -0500 Subject: [PATCH] Refactor withDashboardResources in with-dashboard-resources.tsx into a hook --- .../utilization-card/TopConsumerPopover.tsx | 268 ++++++------ .../console-shared/src/hooks/index.ts | 1 + .../src/hooks/useDashboardResources.ts | 2 +- .../src/hooks/useDynamicK8sWatchResources.ts | 106 +++++ .../baremetal-hosts/dashboard/EventsCard.tsx | 49 +-- .../baremetal-hosts/dashboard/StatusCard.tsx | 21 +- .../cluster-dashboard/activity-card.tsx | 315 +++++++-------- .../cluster-dashboard/details-card.tsx | 380 +++++++++--------- .../cluster-dashboard/health-item.tsx | 353 ++++++++-------- .../cluster-dashboard/inventory-card.tsx | 108 +++-- .../cluster-dashboard/utilization-card.tsx | 289 ++++++------- .../project-dashboard/activity-card.tsx | 258 ++++++------ .../project-dashboard/inventory-card.tsx | 138 +++---- .../dashboard/with-dashboard-resources.tsx | 231 ----------- 14 files changed, 1123 insertions(+), 1396 deletions(-) create mode 100644 frontend/packages/console-shared/src/hooks/useDynamicK8sWatchResources.ts delete mode 100644 frontend/public/components/dashboard/with-dashboard-resources.tsx diff --git a/frontend/packages/console-shared/src/components/dashboard/utilization-card/TopConsumerPopover.tsx b/frontend/packages/console-shared/src/components/dashboard/utilization-card/TopConsumerPopover.tsx index a013bfa1f91..7a2aa56bebb 100644 --- a/frontend/packages/console-shared/src/components/dashboard/utilization-card/TopConsumerPopover.tsx +++ b/frontend/packages/console-shared/src/components/dashboard/utilization-card/TopConsumerPopover.tsx @@ -1,15 +1,11 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import type { FC, ReactNode, ReactText } from 'react'; -import { memo, useState, useCallback, useMemo, useEffect } from 'react'; +import { memo, useState, useCallback, useMemo } from 'react'; import { Button, Popover, PopoverPosition } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom-v5-compat'; import { LIMIT_STATE, Humanize } from '@console/dynamic-plugin-sdk'; import { getPrometheusQueryResponse } from '@console/internal/actions/dashboards'; -import { - withDashboardResources, - DashboardItemProps, -} from '@console/internal/components/dashboard/with-dashboard-resources'; import { DataPoint } from '@console/internal/components/graphs'; import { getInstantVectorStats } from '@console/internal/components/graphs/utils'; import { ConsoleSelect } from '@console/internal/components/utils/console-select'; @@ -17,6 +13,7 @@ import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watc import { resourcePathFromModel } from '@console/internal/components/utils/resource-link'; import { K8sKind, referenceForModel, K8sResourceCommon } from '@console/internal/module/k8s'; import { getName, getNamespace } from '../../..'; +import { useDashboardResources } from '../../../hooks/useDashboardResources'; import { RedExclamationCircleIcon, YellowExclamationTriangleIcon } from '../../status'; import Status from '../status-card/StatusPopup'; @@ -116,157 +113,146 @@ export const LimitsBody: FC = ({ ); }; -export const PopoverBody = withDashboardResources( - memo( - ({ - humanize, - consumers, +export const PopoverBody: FC = memo( + ({ humanize, consumers, namespace, isOpen, description, children }) => { + const { t } = useTranslation(); + const [currentConsumer, setCurrentConsumer] = useState(consumers[0]); + const { query, model, metric, fieldSelector } = currentConsumer; + const k8sResource = useMemo( + () => (isOpen ? getResourceToWatch(model, namespace, fieldSelector) : null), + [fieldSelector, isOpen, model, namespace], + ); + const [consumerData, consumerLoaded, consumersLoadError] = useK8sWatchResource< + K8sResourceCommon[] + >(k8sResource); + + const prometheusQueries = useMemo(() => (isOpen ? [{ query, namespace }] : []), [ + query, namespace, - watchPrometheus, - stopWatchPrometheusQuery, - prometheusResults, isOpen, - description, - children, - }) => { - const { t } = useTranslation(); - const [currentConsumer, setCurrentConsumer] = useState(consumers[0]); - const { query, model, metric, fieldSelector } = currentConsumer; - const k8sResource = useMemo( - () => (isOpen ? getResourceToWatch(model, namespace, fieldSelector) : null), - [fieldSelector, isOpen, model, namespace], - ); - const [consumerData, consumerLoaded, consumersLoadError] = useK8sWatchResource< - K8sResourceCommon[] - >(k8sResource); - useEffect(() => { - if (!isOpen) { - return () => {}; - } - watchPrometheus(query, namespace); - return () => { - stopWatchPrometheusQuery(query); - }; - }, [query, stopWatchPrometheusQuery, watchPrometheus, namespace, isOpen]); + ]); + + const { prometheusResults } = useDashboardResources({ + prometheusQueries, + }); - const top5Data = []; + const top5Data = []; - const [data, error] = getPrometheusQueryResponse(prometheusResults, query); - const bodyData = getInstantVectorStats(data, metric); + const [data, error] = getPrometheusQueryResponse(prometheusResults, query); + const bodyData = getInstantVectorStats(data, metric); - if (k8sResource && consumerLoaded && !consumersLoadError) { - for (const d of bodyData) { - const consumerExists = consumerData.some( - (consumer) => - getName(consumer) === d.metric[metric] && - (model.namespaced ? getNamespace(consumer) === d.metric.namespace : true), - ); - if (consumerExists) { - top5Data.push({ ...d, y: humanize(d.y).string }); - } - if (top5Data.length === 5) { - break; - } + if (k8sResource && consumerLoaded && !consumersLoadError) { + for (const d of bodyData) { + const consumerExists = consumerData.some( + (consumer) => + getName(consumer) === d.metric[metric] && + (model.namespaced ? getNamespace(consumer) === d.metric.namespace : true), + ); + if (consumerExists) { + top5Data.push({ ...d, y: humanize(d.y).string }); + } + if (top5Data.length === 5) { + break; } } + } - const monitoringParams = useMemo(() => { - const params = new URLSearchParams(); - params.set('query0', currentConsumer.query); - if (namespace) { - params.set('namespace', namespace); - } - return params; - }, [currentConsumer.query, namespace]); + const monitoringParams = useMemo(() => { + const params = new URLSearchParams(); + params.set('query0', currentConsumer.query); + if (namespace) { + params.set('namespace', namespace); + } + return params; + }, [currentConsumer.query, namespace]); - const dropdownItems = useMemo( - () => - consumers.reduce((items, curr) => { - items[referenceForModel(curr.model)] = t('console-shared~By {{label}}', { - label: curr.model.labelKey ? t(curr.model.labelKey) : curr.model.label, - }); - return items; - }, {}), - [consumers, t], - ); + const dropdownItems = useMemo( + () => + consumers.reduce((items, curr) => { + items[referenceForModel(curr.model)] = t('console-shared~By {{label}}', { + label: curr.model.labelKey ? t(curr.model.labelKey) : curr.model.label, + }); + return items; + }, {}), + [consumers, t], + ); - const onDropdownChange = useCallback( - (key) => setCurrentConsumer(consumers.find((c) => referenceForModel(c.model) === key)), - [consumers], - ); + const onDropdownChange = useCallback( + (key) => setCurrentConsumer(consumers.find((c) => referenceForModel(c.model) === key)), + [consumers], + ); - const monitoringURL = `/monitoring/query-browser?${monitoringParams.toString()}`; + const monitoringURL = `/monitoring/query-browser?${monitoringParams.toString()}`; - let body: ReactNode; - if (error || consumersLoadError) { - body =
{t('console-shared~Not available')}
; - } else if (!consumerLoaded || !data) { - body = ( -
    -
  • -
  • -
  • -
  • -
  • + let body: ReactNode; + if (error || consumersLoadError) { + body =
    {t('console-shared~Not available')}
    ; + } else if (!consumerLoaded || !data) { + body = ( +
      +
    • +
    • +
    • +
    • +
    • +
    + ); + } else { + body = ( + <> +
      + {top5Data && + top5Data.map((item) => { + const title = String(item.x); + return ( + + + {title} + + + ); + })}
    - ); - } else { - body = ( - <> -
      - {top5Data && - top5Data.map((item) => { - const title = String(item.x); - return ( - - - {title} - - - ); - })} -
    - {t('console-shared~View more')} - - ); - } + {t('console-shared~View more')} + + ); + } - return ( -
    - {description && ( -
    {description}
    - )} - {children} -
    - {consumers.length === 1 - ? t('console-shared~Top {{label}} consumers', { - label: currentConsumer.model.label.toLowerCase(), - }) - : t('console-shared~Top consumers')} -
    - {consumers.length > 1 && ( - - )} - {body} + return ( +
    + {description && ( +
    {description}
    + )} + {children} +
    + {consumers.length === 1 + ? t('console-shared~Top {{label}} consumers', { + label: currentConsumer.model.label.toLowerCase(), + }) + : t('console-shared~Top consumers')}
    - ); - }, - ), + {consumers.length > 1 && ( + + )} + {body} +
    + ); + }, ); const ListItem: FC = ({ children, value }) => ( diff --git a/frontend/packages/console-shared/src/hooks/index.ts b/frontend/packages/console-shared/src/hooks/index.ts index be0ca733595..a5d4d36aa94 100644 --- a/frontend/packages/console-shared/src/hooks/index.ts +++ b/frontend/packages/console-shared/src/hooks/index.ts @@ -36,3 +36,4 @@ export * from './useCopyCodeModal'; export * from './useCopyLoginCommands'; export * from './useQuickStartContext'; export * from './useUser'; +export * from './useDynamicK8sWatchResources'; diff --git a/frontend/packages/console-shared/src/hooks/useDashboardResources.ts b/frontend/packages/console-shared/src/hooks/useDashboardResources.ts index 766bd638b44..592ca678cfa 100644 --- a/frontend/packages/console-shared/src/hooks/useDashboardResources.ts +++ b/frontend/packages/console-shared/src/hooks/useDashboardResources.ts @@ -25,7 +25,7 @@ export const useDashboardResources: UseDashboardResources = ({ prometheusQueries?.forEach((query) => dispatch(watchPrometheusQuery(query.query, null, query.timespan)), ); - urls?.forEach((url) => dispatch(watchURL(url?.url))); + urls?.forEach((url) => dispatch(watchURL(url?.url, url?.fetch))); return () => { prometheusQueries?.forEach((query) => { diff --git a/frontend/packages/console-shared/src/hooks/useDynamicK8sWatchResources.ts b/frontend/packages/console-shared/src/hooks/useDynamicK8sWatchResources.ts new file mode 100644 index 00000000000..3afcb244076 --- /dev/null +++ b/frontend/packages/console-shared/src/hooks/useDynamicK8sWatchResources.ts @@ -0,0 +1,106 @@ +import { useState, useCallback, useMemo } from 'react'; +import * as _ from 'lodash'; +import type { WatchK8sResource, WatchK8sResults } from '@console/dynamic-plugin-sdk'; +import { useK8sWatchResources } from '@console/internal/components/utils/k8s-watch-hook'; + +type UseDynamicK8sWatchResourcesResult = { + results: WatchK8sResults>; + watchResource: (key: string, resource: WatchK8sResource) => void; + stopWatchResource: (key: string) => void; +}; + +/** + * Hook that provides imperative API for dynamically watching K8s resources at runtime. + * + * This is a wrapper around useK8sWatchResources that adds imperative watch/stopWatch callbacks, + * enabling dynamic addition and removal of resources based on runtime conditions (e.g., plugin + * extensions). + * + * Use this hook when: + * - Resources to watch are determined by plugin extensions at runtime + * - The number of resources is unknown or variable + * - You need to conditionally add/remove resources based on props or state changes + * + * For static, predetermined resources, use useK8sWatchResource(s) directly instead. + * + * @returns An object containing: + * - results: Map of resource results keyed by the provided key + * - watchResource: Callback to start watching a resource with explicit key + * - stopWatchResource: Callback to stop watching a resource by key + * + * @example + * ```tsx + * const MyCard = ({ namespace }) => { + * const { results, watchResource, stopWatchResource } = useDynamicK8sWatchResources(); + * + * useEffect(() => { + * watchResource('pods', { kind: 'Pod', namespace, isList: true }); + * return () => stopWatchResource('pods'); + * }, [namespace]); // callbacks are stable, no need to include them + * + * const pods = results.pods?.data; + * const loaded = results.pods?.loaded; + * const error = results.pods?.loadError; + * + * return
    ...
    ; + * }; + * ``` + */ +export const useDynamicK8sWatchResources = (): UseDynamicK8sWatchResourcesResult => { + const [k8sResources, setK8sResources] = useState>({}); + + const resourceResults = useK8sWatchResources(k8sResources); + + /** + * Start watching a K8s resource with explicit key. + * + * @param key - Unique key to identify this resource in results + * @param resource - K8s resource configuration (kind, namespace, isList, etc.) + */ + const watchResource = useCallback((key: string, resource: WatchK8sResource) => { + if (!key) { + // eslint-disable-next-line no-console + console.warn('watchResource called without key - resource will not be watched'); + return; + } + setK8sResources((prev) => { + // Only update if the resource config has changed + const existing = prev[key]; + if (existing && _.isEqual(existing, resource)) { + return prev; + } + return { ...prev, [key]: resource }; + }); + }, []); + + /** + * Stop watching a K8s resource by key. + * + * @param key - Unique key identifying the resource to stop watching + */ + const stopWatchResource = useCallback((key: string) => { + if (!key) { + // eslint-disable-next-line no-console + console.warn('stopWatchResource called without key - no action taken'); + return; + } + + setK8sResources((prev) => { + // Only remove if the key exists to avoid unnecessary state updates + if (!(key in prev)) { + return prev; + } + const { [key]: _removed, ...rest } = prev; + return rest; + }); + }, []); + + return useMemo( + () => ({ + results: resourceResults, + watchResource, + stopWatchResource, + }), + [resourceResults, watchResource, stopWatchResource], + ); +}; diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/EventsCard.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/EventsCard.tsx index 090823b4a12..c740429331e 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/EventsCard.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/EventsCard.tsx @@ -1,19 +1,11 @@ import type { FC } from 'react'; -import { useContext, useEffect } from 'react'; +import { useContext, useMemo } from 'react'; import { Card, CardHeader, CardTitle } from '@patternfly/react-core'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom-v5-compat'; -import { - DashboardItemProps, - withDashboardResources, -} from '@console/internal/components/dashboard/with-dashboard-resources'; -import { - FirehoseResource, - FirehoseResult, - ResourceLink, - resourcePathFromModel, -} from '@console/internal/components/utils'; +import { ResourceLink, resourcePathFromModel } from '@console/internal/components/utils'; +import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { EventModel, MachineModel, NodeModel } from '@console/internal/models'; import { EventKind, K8sResourceKind, MachineKind } from '@console/internal/module/k8s'; import { getName, getNamespace, getMachineNodeName } from '@console/shared'; @@ -27,8 +19,6 @@ import { isHostInProgressState, getBareMetalHostStatus } from '../../../status/h import { BareMetalHostKind } from '../../../types'; import { BareMetalHostDashboardContext } from './BareMetalHostDashboardContext'; -const eventsResource: FirehoseResource = { isList: true, kind: EventModel.kind, prop: 'events' }; - const matchesInvolvedObject = ( kind: string, name: string, @@ -56,15 +46,23 @@ const getHostEventsFilter = ( machine: MachineKind, ): ((event: EventKind) => boolean) => _.partial(hostEventsFilter, host, machine); -const EventsCard: FC = ({ watchK8sResource, stopWatchK8sResource, resources }) => { +const EventsCard: FC = () => { const { t } = useTranslation(); const { obj, machine } = useContext(BareMetalHostDashboardContext); - useEffect(() => { - watchK8sResource(eventsResource); - return () => { - stopWatchK8sResource(eventsResource); - }; - }, [watchK8sResource, stopWatchK8sResource]); + + const [eventsData, eventsLoaded, eventsLoadError] = useK8sWatchResource({ + isList: true, + kind: EventModel.kind, + }); + + const events = useMemo( + () => ({ + data: eventsData, + loaded: eventsLoaded, + loadError: eventsLoadError, + }), + [eventsData, eventsLoaded, eventsLoadError], + ); const filter = getHostEventsFilter(obj, machine); @@ -116,17 +114,10 @@ const EventsCard: FC = ({ watchK8sResource, stopWatchK8sResourc )}
    - } - filter={filter} - /> + ); }; -export default withDashboardResources(EventsCard); - -type EventsCardProps = DashboardItemProps & { - obj: BareMetalHostKind; -}; +export default EventsCard; diff --git a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/StatusCard.tsx b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/StatusCard.tsx index 94ab6490113..77b44066558 100644 --- a/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/StatusCard.tsx +++ b/frontend/packages/metal3-plugin/src/components/baremetal-hosts/dashboard/StatusCard.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react'; -import { useContext, useEffect, useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import { Gallery, GalleryItem, @@ -13,10 +13,6 @@ import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom-v5-compat'; import { Alert, StatusIconAndText } from '@console/dynamic-plugin-sdk'; -import { - DashboardItemProps, - withDashboardResources, -} from '@console/internal/components/dashboard/with-dashboard-resources'; import { alertURL } from '@console/internal/components/monitoring/utils'; import { resourcePathFromModel } from '@console/internal/components/utils'; import { BlueInfoCircleIcon } from '@console/shared'; @@ -27,6 +23,7 @@ import AlertsBody from '@console/shared/src/components/dashboard/status-card/Ale import HealthBody from '@console/shared/src/components/dashboard/status-card/HealthBody'; import HealthItem from '@console/shared/src/components/dashboard/status-card/HealthItem'; import { HealthState } from '@console/shared/src/components/dashboard/status-card/states'; +import { useNotificationAlerts } from '@console/shared/src/hooks/useNotificationAlerts'; import { HOST_STATUS_DESCRIPTION_KEYS, HOST_HARDWARE_ERROR_STATES, @@ -97,20 +94,16 @@ const PowerStatus = ({ obj }: { obj: BareMetalHostKind }) => { ); }; -const HealthCard: FC = ({ watchAlerts, stopWatchAlerts, notificationAlerts }) => { +const HealthCard: FC = () => { const { t } = useTranslation(); const { obj, machine, node, nodeMaintenance } = useContext(BareMetalHostDashboardContext); - useEffect(() => { - watchAlerts(); - return () => stopWatchAlerts(); - }, [watchAlerts, stopWatchAlerts]); + const [data, loaded, loadError] = useNotificationAlerts(); const status = getHostStatus({ host: obj, machine, node, nodeMaintenance }); const hwHealth = getHostHardwareHealthState(obj); - const { data, loaded, loadError } = notificationAlerts || {}; const alerts = useMemo(() => filterAlerts(data), [data]); const hasPowerMgmt = hasPowerManagement(obj); @@ -174,13 +167,9 @@ const HealthCard: FC = ({ watchAlerts, stopWatchAlerts, notific ); }; -export default withDashboardResources(HealthCard); +export default HealthCard; type HostHealthState = { state: HealthState; titleKey: string; }; - -type HealthCardProps = DashboardItemProps & { - obj: BareMetalHostKind; -}; diff --git a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/activity-card.tsx b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/activity-card.tsx index de89dcca57f..d60e512b666 100644 --- a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/activity-card.tsx +++ b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/activity-card.tsx @@ -7,9 +7,11 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom-v5-compat'; import { Card, CardHeader, CardTitle, CardFooter, Divider } from '@patternfly/react-core'; -import { DashboardItemProps, withDashboardResources } from '../../with-dashboard-resources'; +import { useDynamicK8sWatchResources } from '@console/shared/src/hooks/useDynamicK8sWatchResources'; +import { useDashboardResources } from '@console/shared/src/hooks/useDashboardResources'; +import { useK8sWatchResource } from '../../../utils/k8s-watch-hook'; import { EventModel } from '../../../../models'; -import { FirehoseResource, FirehoseResult } from '../../../utils/types'; +import { FirehoseResult } from '../../../utils/types'; import { EventKind, K8sKind } from '../../../../module/k8s'; import ActivityBody, { RecentEventsBody, @@ -25,180 +27,156 @@ import { import { uniqueResource } from './utils'; import { PrometheusResponse } from '../../../graphs'; -const eventsResource: FirehoseResource = { isList: true, kind: EventModel.kind, prop: 'events' }; const viewEvents = '/k8s/all-namespaces/events'; -const RecentEvent = withDashboardResources( - ({ watchK8sResource, stopWatchK8sResource, resources }) => { - useEffect(() => { - watchK8sResource(eventsResource); - return () => { - stopWatchK8sResource(eventsResource); - }; - }, [watchK8sResource, stopWatchK8sResource]); - return ( - } - moreLink={viewEvents} - /> - ); - }, -); +const RecentEvent: FC = () => { + const { t } = useTranslation(); + + const [eventsData, eventsLoaded, eventsLoadError] = useK8sWatchResource({ + isList: true, + kind: EventModel.kind, + }); + + const events: FirehoseResult = useMemo( + () => ({ + data: eventsData, + loaded: eventsLoaded, + loadError: eventsLoadError, + }), + [eventsData, eventsLoaded, eventsLoadError], + ); + + const shouldShowFooter = events?.loaded && events?.data && events.data.length > 50; + + return ( + <> + + {shouldShowFooter && ( + <> + + + + {t('public~View all events')} + + + + )} + + ); +}; const mapStateToProps = ({ k8s }) => ({ models: k8s.getIn(['RESOURCES', 'models']), }); -const OngoingActivity = connect(mapStateToProps)( - withDashboardResources( - ({ - watchK8sResource, - stopWatchK8sResource, - resources, - watchPrometheus, - stopWatchPrometheusQuery, - prometheusResults, - models, - }: DashboardItemProps & OngoingActivityProps) => { - const [resourceActivityExtensions] = useResolvedExtensions< - DashboardsOverviewResourceActivity - >(isDashboardsOverviewResourceActivity); - - const resourceActivities = useMemo( - () => resourceActivityExtensions.filter((e) => !!models.get(e.properties.k8sResource.kind)), - [resourceActivityExtensions, models], - ); - - const [prometheusActivities] = useResolvedExtensions( - isDashboardsOverviewPrometheusActivity, - ); - - useEffect(() => { - resourceActivities.forEach((a, index) => { - watchK8sResource(uniqueResource(a.properties.k8sResource, index)); - }); - prometheusActivities.forEach((a) => - a.properties.queries.forEach((q) => watchPrometheus(q)), - ); - return () => { - resourceActivities.forEach((a, index) => { - stopWatchK8sResource(uniqueResource(a.properties.k8sResource, index)); - }); - prometheusActivities.forEach((a) => - a.properties.queries.forEach(stopWatchPrometheusQuery), +const OngoingActivityComponent: FC = ({ models }) => { + const { watchResource, stopWatchResource, results: resources } = useDynamicK8sWatchResources(); + + const [resourceActivityExtensions] = useResolvedExtensions( + isDashboardsOverviewResourceActivity, + ); + + const resourceActivities = useMemo( + () => resourceActivityExtensions.filter((e) => !!models.get(e.properties.k8sResource.kind)), + [resourceActivityExtensions, models], + ); + + const [prometheusActivities] = useResolvedExtensions( + isDashboardsOverviewPrometheusActivity, + ); + + const prometheusQueries = useMemo( + () => prometheusActivities.flatMap((a) => a.properties.queries.map((query) => ({ query }))), + [prometheusActivities], + ); + + const { prometheusResults } = useDashboardResources({ prometheusQueries }); + + useEffect(() => { + resourceActivities.forEach((a, index) => { + const uniqueRes = uniqueResource(a.properties.k8sResource, index); + const { prop, ...resourceConfig } = uniqueRes; + watchResource(prop, resourceConfig); + }); + return () => { + resourceActivities.forEach((a, index) => { + const resourceKey = uniqueResource(a.properties.k8sResource, index).prop; + stopWatchResource(resourceKey); + }); + }; + }, [watchResource, stopWatchResource, resourceActivities]); + + const allResourceActivities = useMemo( + () => + _.flatten( + resourceActivities.map((a, index) => { + const k8sResources = _.get( + resources, + [uniqueResource(a.properties.k8sResource, index).prop, 'data'], + [], + ) as FirehoseResult['data']; + return k8sResources + .filter((r) => (a.properties.isActivity ? a.properties.isActivity(r) : true)) + .map((r) => ({ + resource: r, + timestamp: a.properties.getTimestamp ? a.properties.getTimestamp(r) : null, + component: a.properties.component, + })); + }), + ), + [resourceActivities, resources], + ); + + const allPrometheusActivities = useMemo( + () => + prometheusActivities + .filter((a) => { + const queryResults = a.properties.queries.map( + (q) => prometheusResults.getIn([q, 'data']) as PrometheusResponse, ); - }; - }, [ - watchK8sResource, - stopWatchK8sResource, - watchPrometheus, - stopWatchPrometheusQuery, - resourceActivities, - prometheusActivities, - ]); - - const allResourceActivities = useMemo( - () => - _.flatten( - resourceActivities.map((a, index) => { - const k8sResources = _.get( - resources, - [uniqueResource(a.properties.k8sResource, index).prop, 'data'], - [], - ) as FirehoseResult['data']; - return k8sResources - .filter((r) => (a.properties.isActivity ? a.properties.isActivity(r) : true)) - .map((r) => ({ - resource: r, - timestamp: a.properties.getTimestamp ? a.properties.getTimestamp(r) : null, - component: a.properties.component, - })); - }), - ), - [resourceActivities, resources], - ); - - const allPrometheusActivities = useMemo( - () => - prometheusActivities - .filter((a) => { - const queryResults = a.properties.queries.map( - (q) => prometheusResults.getIn([q, 'data']) as PrometheusResponse, - ); - return a.properties.isActivity(queryResults); - }) - .map((a) => { - const queryResults = a.properties.queries.map( - (q) => prometheusResults.getIn([q, 'data']) as PrometheusResponse, - ); - return { - component: a.properties.component, - results: queryResults, - }; - }), - [prometheusActivities, prometheusResults], - ); - - const resourcesLoaded = useMemo( - () => - resourceActivities.every((a, index) => { - const uniqueProp = uniqueResource(a.properties.k8sResource, index).prop; - return resources[uniqueProp]?.loaded || resources[uniqueProp]?.loadError; - }), - [resourceActivities, resources], - ); - - const queriesLoaded = useMemo( - () => - prometheusActivities.every((a) => - a.properties.queries.every( - (q) => - prometheusResults.getIn([q, 'data']) || prometheusResults.getIn([q, 'loadError']), - ), - ), - [prometheusActivities, prometheusResults], - ); - - return ( - - ); - }, - ), -); - -const RecentEventFooter = withDashboardResources( - ({ watchK8sResource, stopWatchK8sResource, resources }) => { - const { t } = useTranslation(); - useEffect(() => { - watchK8sResource(eventsResource); - return () => { - stopWatchK8sResource(eventsResource); - }; - }, [watchK8sResource, stopWatchK8sResource]); - - const events = resources.events as FirehoseResult; - const shouldShowFooter = events?.loaded && events?.data && events.data.length > 50; - - if (!shouldShowFooter) { - return null; - } - - return ( - <> - - - - {t('public~View all events')} - - - - ); - }, -); + return a.properties.isActivity(queryResults); + }) + .map((a) => { + const queryResults = a.properties.queries.map( + (q) => prometheusResults.getIn([q, 'data']) as PrometheusResponse, + ); + return { + component: a.properties.component, + results: queryResults, + }; + }), + [prometheusActivities, prometheusResults], + ); + + const resourcesLoaded = useMemo( + () => + resourceActivities.every((a, index) => { + const uniqueProp = uniqueResource(a.properties.k8sResource, index).prop; + return resources[uniqueProp]?.loaded || resources[uniqueProp]?.loadError; + }), + [resourceActivities, resources], + ); + + const queriesLoaded = useMemo( + () => + prometheusActivities.every((a) => + a.properties.queries.every( + (q) => prometheusResults.getIn([q, 'data']) || prometheusResults.getIn([q, 'loadError']), + ), + ), + [prometheusActivities, prometheusResults], + ); + + return ( + + ); +}; + +const OngoingActivity = connect(mapStateToProps)(OngoingActivityComponent); export const ActivityCard: FC<{}> = memo(() => { const { t } = useTranslation(); @@ -212,7 +190,6 @@ export const ActivityCard: FC<{}> = memo(() => { - ); }); diff --git a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/details-card.tsx b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/details-card.tsx index 72081340b7f..c2943ffe1d2 100644 --- a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/details-card.tsx +++ b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/details-card.tsx @@ -22,8 +22,6 @@ import { OverviewDetailItem as OverviewDetailItemType, } from '@console/dynamic-plugin-sdk'; import { OverviewDetailItem } from '@console/internal/components/overview/OverviewDetailItem'; - -import { DashboardItemProps, withDashboardResources } from '../../with-dashboard-resources'; import { ClusterVersionModel } from '../../../../models'; import { ServiceLevel, @@ -101,217 +99,211 @@ const clusterVersionResource: WatchK8sResource = { isList: false, }; -export const DetailsCard = withDashboardResources( - ({ watchK8sResource, stopWatchK8sResource }: DetailsCardProps) => { - const { t } = useTranslation(); - const openshiftFlag = useFlag(FLAGS.OPENSHIFT); - const { infrastructure, infrastructureLoaded, infrastructureError } = useContext( - ClusterDashboardContext, - ); - const [k8sVersion, setK8sVersion] = useState(); - const [k8sVersionError, setK8sVersionError] = useState(); - const [clusterVersionData, clusterVersionLoaded, clusterVersionError] = useK8sWatchResource< - ClusterVersionKind - >(clusterVersionResource); - const [detailItemsExtensions] = useResolvedExtensions( - isOverviewDetailItem, - ); - const [customDetailItemsExtensions] = useResolvedExtensions( - isCustomOverviewDetailItem, - ); +export const DetailsCard: React.FC = () => { + const { t } = useTranslation(); + const openshiftFlag = useFlag(FLAGS.OPENSHIFT); + const { infrastructure, infrastructureLoaded, infrastructureError } = useContext( + ClusterDashboardContext, + ); + const [k8sVersion, setK8sVersion] = useState(); + const [k8sVersionError, setK8sVersionError] = useState(); + const [clusterVersionData, clusterVersionLoaded, clusterVersionError] = useK8sWatchResource< + ClusterVersionKind + >(clusterVersionResource); + const [detailItemsExtensions] = useResolvedExtensions( + isOverviewDetailItem, + ); + const [customDetailItemsExtensions] = useResolvedExtensions( + isCustomOverviewDetailItem, + ); - useEffect(() => { - if (flagPending(openshiftFlag)) { - return; + useEffect(() => { + if (flagPending(openshiftFlag)) { + return; + } + const fetchK8sVersion = async () => { + try { + const version = await fetch('version'); + setK8sVersion(version); + } catch (error) { + setK8sVersionError(error); } - const fetchK8sVersion = async () => { - try { - const version = await fetch('version'); - setK8sVersion(version); - } catch (error) { - setK8sVersionError(error); - } - }; - fetchK8sVersion(); - }, [openshiftFlag, watchK8sResource, stopWatchK8sResource]); - const serviceLevelTitle = useServiceLevelTitle(); + }; + fetchK8sVersion(); + }, [openshiftFlag]); + const serviceLevelTitle = useServiceLevelTitle(); - const clusterID = getClusterID(clusterVersionData); - const openShiftVersion = getOpenShiftVersion(clusterVersionData); - const cvChannel = getClusterVersionChannel(clusterVersionData); + const clusterID = getClusterID(clusterVersionData); + const openShiftVersion = getOpenShiftVersion(clusterVersionData); + const cvChannel = getClusterVersionChannel(clusterVersionData); - const infrastructurePlatform = getInfrastructurePlatform(infrastructure); - const infrastuctureApiUrl = getInfrastructureAPIURL(infrastructure); + const infrastructurePlatform = getInfrastructurePlatform(infrastructure); + const infrastuctureApiUrl = getInfrastructureAPIURL(infrastructure); - const k8sGitVersion = getK8sGitVersion(k8sVersion); + const k8sGitVersion = getK8sGitVersion(k8sVersion); - return ( - - + + + {t('public~View settings')} + + + ), + hasNoOffset: false, + className: 'co-overview-card__actions', + }} + > + {t('public~Details')} + + + {flagPending(openshiftFlag) ? ( + + ) : ( + + {openshiftFlag ? ( <> - - {t('public~View settings')} - - - ), - hasNoOffset: false, - className: 'co-overview-card__actions', - }} - > - {t('public~Details')} - - - {flagPending(openshiftFlag) ? ( - - ) : ( - - {openshiftFlag ? ( - <> - - {infrastuctureApiUrl} - - -
    {clusterID}
    - {window.SERVER_FLAGS.branding !== 'okd' && - window.SERVER_FLAGS.branding !== 'azure' && ( - - )} -
    - - {infrastructurePlatform} - - - - + + {infrastuctureApiUrl} + + +
    {clusterID}
    + {window.SERVER_FLAGS.branding !== 'okd' && + window.SERVER_FLAGS.branding !== 'azure' && ( + + )} +
    + + {infrastructurePlatform} + + + + - - - - } - > + - {/* Service Level handles loading and error state */} - + - - - - {cvChannel} + } + > + + {/* Service Level handles loading and error state */} + - {isSingleNode(infrastructure) && ( - - {t('public~No (single control plane node)')} - - )} - {detailItemsExtensions.map((e) => { - const Component = e.properties.component; - return ( - ( - {children} - )} - > - - - ); - })} - {customDetailItemsExtensions.map((e) => { - const { component: Component, error, isLoading, ...props } = e.properties; - return ( - ( - {children} - )} - > - - - - - ); - })} - - ) : ( + + - {k8sGitVersion} + {cvChannel} - )} -
    - )} -
    -
    - ); - }, -); - -type DetailsCardProps = DashboardItemProps & { - openshiftFlag: boolean; + {isSingleNode(infrastructure) && ( + + {t('public~No (single control plane node)')} + + )} + {detailItemsExtensions.map((e) => { + const Component = e.properties.component; + return ( + ( + {children} + )} + > + + + ); + })} + {customDetailItemsExtensions.map((e) => { + const { component: Component, error, isLoading, ...props } = e.properties; + return ( + ( + {children} + )} + > + + + + + ); + })} + + ) : ( + + {k8sGitVersion} + + )} + + )} + + + ); }; type ClusterVersionProps = { 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 29b54f0a955..e2599101de8 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 @@ -25,12 +25,13 @@ import { HealthState, healthStateMessage, } from '@console/shared/src/components/dashboard/status-card/states'; +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 { useK8sWatchResources } from '../../../utils/k8s-watch-hook'; -import { withDashboardResources, DashboardItemProps } from '../../with-dashboard-resources'; +import { useK8sWatchResource, useK8sWatchResources } from '../../../utils/k8s-watch-hook'; import { uniqueResource } from './utils'; import { getPrometheusQueryResponse } from '../../../../actions/dashboards'; import { ClusterDashboardContext } from './context'; @@ -91,197 +92,167 @@ export const OperatorsPopup: FC = ({ resources, operatorSub ); }; -export const OperatorHealthItem = withDashboardResources( - ({ resources, watchK8sResource, stopWatchK8sResource, operatorSubsystems }) => { - const { t } = useTranslation(); - useEffect(() => { +export const OperatorHealthItem: FC = ({ operatorSubsystems }) => { + const { t } = useTranslation(); + const { watchResource, stopWatchResource, results: resources } = useDynamicK8sWatchResources(); + + useEffect(() => { + operatorSubsystems.forEach((o, index) => + o.resources.forEach((r) => { + const uniqueRes = uniqueResource(r, index); + const { prop, ...resourceConfig } = uniqueRes; + watchResource(prop, resourceConfig); + }), + ); + return () => { operatorSubsystems.forEach((o, index) => - o.resources.forEach((r) => watchK8sResource(uniqueResource(r, index))), + o.resources.forEach((r) => { + const resourceKey = uniqueResource(r, index).prop; + stopWatchResource(resourceKey); + }), ); - return () => { - operatorSubsystems.forEach((o, index) => - o.resources.forEach((r) => stopWatchK8sResource(uniqueResource(r, index))), - ); - }; - }, [watchK8sResource, stopWatchK8sResource, operatorSubsystems]); - - const healthStatuses = operatorSubsystems.map((o, index) => { - const operatorResources = o.resources.reduce((acc, r) => { - acc[r.prop] = resources[uniqueResource(r, index).prop] || {}; - return acc; - }, {}); - if ( - Object.keys(operatorResources).some((resource) => operatorResources[resource].loadError) - ) { - return { health: HealthState.NOT_AVAILABLE }; - } - if (Object.keys(operatorResources).some((resource) => !operatorResources[resource].loaded)) { - return { health: HealthState.LOADING }; - } - const operatorStatuses = o.getOperatorsWithStatuses(operatorResources); - if (!operatorStatuses.length) { - return { health: HealthState.OK }; - } - const importantStatuses = getMostImportantStatuses(operatorStatuses); - return { - health: importantStatuses[0].status.health, - count: importantStatuses.length, - }; - }); - - const operatorsHealth = getOperatorsHealthState(healthStatuses, t); - - return ( - - - - ); - }, -); - -export const URLHealthItem = withDashboardResources( - ({ - watchURL, - stopWatchURL, - urlResults, - resources, - watchK8sResource, - stopWatchK8sResource, - subsystem, - models, - }) => { - const { t } = useTranslation(); - - const modelExists = - subsystem.additionalResource && !!models.get(subsystem.additionalResource.kind); - useEffect(() => { - watchURL(subsystem.url, subsystem.fetch); - if (modelExists) { - watchK8sResource(subsystem.additionalResource); - } - return () => { - stopWatchURL(subsystem.url); - if (modelExists) { - stopWatchK8sResource(subsystem.additionalResource); - } - }; - }, [watchURL, stopWatchURL, watchK8sResource, stopWatchK8sResource, subsystem, modelExists]); - - const healthResult = urlResults.getIn([subsystem.url, 'data']); - const healthResultError = urlResults.getIn([subsystem.url, 'loadError']); - - const k8sResult = subsystem.additionalResource - ? resources[subsystem.additionalResource.prop] - : null; - const healthState = subsystem.healthHandler?.(healthResult, healthResultError, k8sResult) ?? { - state: HealthState.NOT_AVAILABLE, - message: healthStateMessage(HealthState.NOT_AVAILABLE, t), }; + }, [watchResource, stopWatchResource, operatorSubsystems]); - const PopupComponent = subsystem?.popupComponent; - - return ( - - {PopupComponent && ( - - )} - - ); - }, -); - -export const PrometheusHealthItem = withDashboardResources( - ({ - watchK8sResource, - stopWatchK8sResource, - resources, - watchPrometheus, - stopWatchPrometheusQuery, - prometheusResults, - subsystem, - models, - }) => { - const { t } = useTranslation(); - const { infrastructure } = useContext(ClusterDashboardContext); - - const modelExists = - subsystem.additionalResource && !!models.get(subsystem.additionalResource.kind); - useEffect(() => { - subsystem.queries.forEach((q) => watchPrometheus(q)); - if (modelExists) { - watchK8sResource(subsystem.additionalResource); + const healthStatuses = operatorSubsystems.map((o, index) => { + const operatorResources = o.resources.reduce((acc, r) => { + acc[r.prop] = resources[uniqueResource(r, index).prop] || {}; + return acc; + }, {}); + if (Object.keys(operatorResources).some((resource) => operatorResources[resource].loadError)) { + return { health: HealthState.NOT_AVAILABLE }; + } + if (Object.keys(operatorResources).some((resource) => !operatorResources[resource].loaded)) { + return { health: HealthState.LOADING }; + } + const operatorStatuses = o.getOperatorsWithStatuses(operatorResources); + if (!operatorStatuses.length) { + return { health: HealthState.OK }; + } + const importantStatuses = getMostImportantStatuses(operatorStatuses); + return { + health: importantStatuses[0].status.health, + count: importantStatuses.length, + }; + }); + + const operatorsHealth = getOperatorsHealthState(healthStatuses, t); + + return ( + + + + ); +}; + +export const URLHealthItem: FC = ({ subsystem, models }) => { + const { t } = useTranslation(); + + const urls = useMemo(() => [{ url: subsystem.url, fetch: subsystem.fetch }], [ + subsystem.url, + subsystem.fetch, + ]); + const { urlResults } = useDashboardResources({ urls }); + const modelExists = + subsystem.additionalResource && !!models.get(subsystem.additionalResource.kind); + const [k8sData, k8sLoaded, k8sLoadError] = useK8sWatchResource( + modelExists ? subsystem.additionalResource : null, + ); + + const healthResult = urlResults.getIn([subsystem.url, 'data']); + const healthResultError = urlResults.getIn([subsystem.url, 'loadError']); + + const k8sResult = modelExists + ? { data: k8sData, loaded: k8sLoaded, loadError: k8sLoadError } + : null; + + const healthState = subsystem.healthHandler?.(healthResult, healthResultError, k8sResult) ?? { + state: HealthState.NOT_AVAILABLE, + message: healthStateMessage(HealthState.NOT_AVAILABLE, t), + }; + + const PopupComponent = subsystem?.popupComponent; + + return ( + + {PopupComponent && ( + + )} + + ); +}; + +export const PrometheusHealthItem: FC = ({ subsystem, models }) => { + const { t } = useTranslation(); + const { infrastructure } = useContext(ClusterDashboardContext); + + const prometheusQueries = useMemo(() => subsystem.queries.map((query) => ({ query })), [ + subsystem.queries, + ]); + const { prometheusResults } = useDashboardResources({ prometheusQueries }); + + const modelExists = + subsystem.additionalResource && !!models.get(subsystem.additionalResource.kind); + const [k8sData, k8sLoaded, k8sLoadError] = useK8sWatchResource( + modelExists ? subsystem.additionalResource : null, + ); + + const queryResults = useMemo( + () => + subsystem.queries.map((q) => { + const [response, error] = getPrometheusQueryResponse(prometheusResults, q); + return { + response, + error, + }; + }), + [prometheusResults, subsystem.queries], + ); + + // Format K8s result to match expected structure + const k8sResult = modelExists + ? { data: k8sData, loaded: k8sLoaded, loadError: k8sLoadError } + : null; + + const healthState: SubsystemHealth = subsystem.healthHandler?.( + queryResults, + t, + k8sResult, + infrastructure, + ) ?? { state: HealthState.NOT_AVAILABLE, message: 'Health handler not available' }; + + const PopupComponent = subsystem?.popupComponent; + + return ( + + : undefined } - return () => { - subsystem.queries.forEach((q) => stopWatchPrometheusQuery(q)); - if (modelExists) { - stopWatchK8sResource(subsystem.additionalResource); - } - }; - }, [ - watchK8sResource, - stopWatchK8sResource, - watchPrometheus, - stopWatchPrometheusQuery, - subsystem, - modelExists, - ]); - - const queryResults = useMemo( - () => - subsystem.queries.map((q) => { - const [response, error] = getPrometheusQueryResponse(prometheusResults, q); - return { - response, - error, - }; - }), - [prometheusResults, subsystem.queries], - ); - const k8sResult = subsystem.additionalResource - ? resources[subsystem.additionalResource.prop] - : null; - const healthState: SubsystemHealth = subsystem.healthHandler?.( - queryResults, - t, - k8sResult, - infrastructure, - ) ?? { state: HealthState.NOT_AVAILABLE, message: 'Health handler not available' }; - - const PopupComponent = subsystem?.popupComponent; - - return ( - ( - - ) - : undefined - } - /> - ); - }, -); + /> + ); +}; export const ResourceHealthItem: FC = ({ subsystem, namespace }) => { const { t } = useTranslation(); @@ -319,16 +290,16 @@ export const ResourceHealthItem: FC = ({ subsystem, nam ); }; -type OperatorHealthItemProps = DashboardItemProps & { +type OperatorHealthItemProps = { operatorSubsystems: ResolvedExtension['properties'][]; }; -type URLHealthItemProps = DashboardItemProps & { +type URLHealthItemProps = { subsystem: ResolvedExtension>['properties']; models: ImmutableMap; }; -type PrometheusHealthItemProps = DashboardItemProps & { +type PrometheusHealthItemProps = { subsystem: ResolvedExtension['properties']; models: ImmutableMap; }; 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 9b49f756a3f..3094628c4c8 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 @@ -7,7 +7,7 @@ import { } from '@console/shared/src/components/dashboard/inventory-card/InventoryItem'; import { ErrorBoundary } from '@console/shared/src/components/error'; -import { DashboardItemProps, withDashboardResources } from '../../with-dashboard-resources'; +// Migrated from withDashboardResources HOC to direct hook usage import { K8sKind, referenceForModel, K8sResourceCommon } from '../../../../module/k8s'; import { useResolvedExtensions, @@ -36,63 +36,61 @@ const getFirehoseResource = (model: K8sKind) => ({ prop: 'resource', }); -const ClusterInventoryItem = withDashboardResources( - memo( - ({ model, resolvedMapper, mapperLoader, additionalResources }: ClusterInventoryItemProps) => { - const mainResource = useMemo(() => getFirehoseResource(model), [model]); - const otherResources = useMemo(() => additionalResources || {}, [additionalResources]); - const [mapper, setMapper] = useState(); - const [resourceData, resourceLoaded, resourceLoadError] = useK8sWatchResource< - K8sResourceCommon[] - >(mainResource); - const resources = useK8sWatchResources(otherResources); - useEffect(() => { - mapperLoader && - mapperLoader() - .then((res) => setMapper(() => res)) - .catch(() => { - // eslint-disable-next-line no-console - console.error('Mapper does not exist in module'); - }); - }, [mapperLoader]); +const ClusterInventoryItem = memo( + ({ model, resolvedMapper, mapperLoader, additionalResources }) => { + const mainResource = useMemo(() => getFirehoseResource(model), [model]); + const otherResources = useMemo(() => additionalResources || {}, [additionalResources]); + const [mapper, setMapper] = useState(); + const [resourceData, resourceLoaded, resourceLoadError] = useK8sWatchResource< + K8sResourceCommon[] + >(mainResource); + const resources = useK8sWatchResources(otherResources); + useEffect(() => { + mapperLoader && + mapperLoader() + .then((res) => setMapper(() => res)) + .catch(() => { + // eslint-disable-next-line no-console + console.error('Mapper does not exist in module'); + }); + }, [mapperLoader]); - const [ - additionalResourcesData, - additionalResourcesLoaded, - additionalResourcesLoadError, - ] = useMemo(() => { - const resourcesData = {}; - let resourcesLoaded = true; - let resourcesLoadError = false; + const [ + additionalResourcesData, + additionalResourcesLoaded, + additionalResourcesLoadError, + ] = useMemo(() => { + const resourcesData = {}; + let resourcesLoaded = true; + let resourcesLoadError = false; - if (additionalResources) { - resourcesLoaded = Object.keys(additionalResources) - .filter((key) => !additionalResources[key].optional) - .every((key) => resources[key].loaded); - Object.keys(additionalResources).forEach((key) => { - resourcesData[key] = resources[key].data; - }); - resourcesLoadError = Object.keys(additionalResources) - .filter((key) => !additionalResources[key].optional) - .some((key) => !!resources[key].loadError); - } + if (additionalResources) { + resourcesLoaded = Object.keys(additionalResources) + .filter((key) => !additionalResources[key].optional) + .every((key) => resources[key].loaded); + Object.keys(additionalResources).forEach((key) => { + resourcesData[key] = resources[key].data; + }); + resourcesLoadError = Object.keys(additionalResources) + .filter((key) => !additionalResources[key].optional) + .some((key) => !!resources[key].loadError); + } - return [resourcesData, resourcesLoaded, resourcesLoadError]; - }, [additionalResources, resources]); + return [resourcesData, resourcesLoaded, resourcesLoadError]; + }, [additionalResources, resources]); - return ( - - ); - }, - ), + return ( + + ); + }, ); export const InventoryCard = () => { @@ -144,7 +142,7 @@ export const InventoryCard = () => { ); }; -type ClusterInventoryItemProps = DashboardItemProps & { +type ClusterInventoryItemProps = { model: K8sKind; mapperLoader?: () => Promise; resolvedMapper?: StatusGroupMapper; diff --git a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/utilization-card.tsx b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/utilization-card.tsx index aa6fa08c545..891e3c0e2ff 100644 --- a/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/utilization-card.tsx +++ b/frontend/public/components/dashboard/dashboards-page/cluster-dashboard/utilization-card.tsx @@ -1,5 +1,5 @@ import type { FC, Ref, MouseEvent, ComponentType } from 'react'; -import { useEffect, useState, useMemo } from 'react'; +import { useState, useMemo } from 'react'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { @@ -33,7 +33,7 @@ import UtilizationItem, { import { UtilizationBody } from '@console/shared/src/components/dashboard/utilization-card/UtilizationBody'; import { ByteDataTypes } from '@console/shared/src/graph-helper/data-utils'; -import { DashboardItemProps, withDashboardResources } from '../../with-dashboard-resources'; +import { useDashboardResources } from '@console/shared/src/hooks/useDashboardResources'; import { humanizeBinaryBytes, humanizeCpuCores, @@ -65,154 +65,135 @@ import { const networkPopovers = [NetworkInPopover, NetworkOutPopover]; -export const PrometheusUtilizationItem = withDashboardResources( - ({ - watchPrometheus, - stopWatchPrometheusQuery, - prometheusResults, - utilizationQuery, - totalQuery, - title, - TopConsumerPopover, - humanizeValue, - byteDataType, - namespace, - isDisabled = false, - limitQuery, - requestQuery, - setLimitReqState, - }) => { - let utilization: PrometheusResponse, utilizationError: any; - let total: PrometheusResponse, totalError: any; - let max: DataPoint[]; - let limit: PrometheusResponse, limitError: any; - let request: PrometheusResponse, requestError: any; - let isLoading = false; - const { duration } = useUtilizationDuration(); +export const PrometheusUtilizationItem: React.FC = ({ + utilizationQuery, + totalQuery, + title, + TopConsumerPopover, + humanizeValue, + byteDataType, + // namespace, + isDisabled = false, + limitQuery, + requestQuery, + setLimitReqState, +}) => { + const { duration } = useUtilizationDuration(); - useEffect(() => { - if (!isDisabled) { - watchPrometheus(utilizationQuery, namespace, duration); - totalQuery && watchPrometheus(totalQuery, namespace); - limitQuery && watchPrometheus(limitQuery, namespace, duration); - requestQuery && watchPrometheus(requestQuery, namespace, duration); - return () => { - stopWatchPrometheusQuery(utilizationQuery, duration); - totalQuery && stopWatchPrometheusQuery(totalQuery); - limitQuery && stopWatchPrometheusQuery(limitQuery, duration); - requestQuery && stopWatchPrometheusQuery(requestQuery, duration); - }; - } - }, [ - watchPrometheus, - stopWatchPrometheusQuery, - duration, - utilizationQuery, - totalQuery, - namespace, - isDisabled, - limitQuery, - requestQuery, - ]); + const queries = useMemo(() => { + if (isDisabled) { + return []; + } + const result = [ + { query: utilizationQuery, timespan: duration }, + totalQuery && { query: totalQuery }, + limitQuery && { query: limitQuery, timespan: duration }, + requestQuery && { query: requestQuery, timespan: duration }, + ].filter(Boolean); + return result as { query: string; timespan?: number }[]; + }, [isDisabled, utilizationQuery, totalQuery, limitQuery, requestQuery, duration]); - if (!isDisabled) { - [utilization, utilizationError] = getPrometheusQueryResponse( - prometheusResults, - utilizationQuery, - duration, - ); - [total, totalError] = getPrometheusQueryResponse(prometheusResults, totalQuery); - [limit, limitError] = getPrometheusQueryResponse(prometheusResults, limitQuery, duration); - [request, requestError] = getPrometheusQueryResponse( - prometheusResults, - requestQuery, - duration, - ); + const dashboardResources = useDashboardResources({ + prometheusQueries: queries, + }); + const prometheusResults = dashboardResources.prometheusResults; - max = getInstantVectorStats(total); - isLoading = !utilization || (totalQuery && !total) || (limitQuery && !limit); - } + let utilization: PrometheusResponse, utilizationError: any; + let total: PrometheusResponse, totalError: any; + let max: DataPoint[]; + let limit: PrometheusResponse, limitError: any; + let request: PrometheusResponse, requestError: any; + let isLoading = false; - return ( - + if (!isDisabled) { + [utilization, utilizationError] = getPrometheusQueryResponse( + prometheusResults, + utilizationQuery, + duration, ); - }, -); + [total, totalError] = getPrometheusQueryResponse(prometheusResults, totalQuery); + [limit, limitError] = getPrometheusQueryResponse(prometheusResults, limitQuery, duration); + [request, requestError] = getPrometheusQueryResponse(prometheusResults, requestQuery, duration); -export const PrometheusMultilineUtilizationItem = withDashboardResources< - PrometheusMultilineUtilizationItemProps ->( - ({ - watchPrometheus, - stopWatchPrometheusQuery, - prometheusResults, - queries, - title, - TopConsumerPopovers, - humanizeValue, - byteDataType, - namespace, - isDisabled = false, - }) => { - const { duration } = useUtilizationDuration(); - useEffect(() => { - if (!isDisabled) { - queries.forEach((q) => watchPrometheus(q.query, namespace, duration)); - return () => { - queries.forEach((q) => stopWatchPrometheusQuery(q.query, duration)); - }; - } - }, [watchPrometheus, stopWatchPrometheusQuery, duration, queries, namespace, isDisabled]); + max = getInstantVectorStats(total); + isLoading = !utilization || (totalQuery && !total) || (limitQuery && !limit); + } + + return ( + + ); +}; + +export const PrometheusMultilineUtilizationItem: React.FC = ({ + queries, + title, + TopConsumerPopovers, + humanizeValue, + byteDataType, + isDisabled = false, +}) => { + const { duration } = useUtilizationDuration(); - const stats = []; - let hasError = false; - let isLoading = false; - if (!isDisabled) { - queries.forEach((query) => { - const [response, responseError] = getPrometheusQueryResponse( - prometheusResults, - query.query, - duration, - ); - if (responseError) { - hasError = true; - return false; - } - if (!response) { - isLoading = true; - return false; - } - stats.push(getRangeVectorStats(response, query.desc, null, trimSecondsXMutator)?.[0] || []); - }); + const prometheusQueries = useMemo(() => { + if (isDisabled) { + return []; } + return queries.map((q) => ({ query: q.query, timespan: duration })); + }, [isDisabled, queries, duration]); - return ( - - ); - }, -); + const dashboardResources = useDashboardResources({ + prometheusQueries, + }); + const prometheusResults = dashboardResources.prometheusResults; + + const stats = []; + let hasError = false; + let isLoading = false; + if (!isDisabled) { + queries.forEach((query) => { + const [response, responseError] = getPrometheusQueryResponse( + prometheusResults, + query.query, + duration, + ); + if (responseError) { + hasError = true; + return false; + } + if (!response) { + isLoading = true; + return false; + } + stats.push(getRangeVectorStats(response, query.desc, null, trimSecondsXMutator)?.[0] || []); + }); + } + + return ( + + ); +}; const UtilizationCardNodeFilter: FC = ({ machineConfigPools, @@ -416,21 +397,19 @@ type PrometheusCommonProps = { isDisabled?: boolean; }; -type PrometheusUtilizationItemProps = DashboardItemProps & - PrometheusCommonProps & { - utilizationQuery: string; - totalQuery?: string; - limitQuery?: string; - requestQuery?: string; - TopConsumerPopover?: ComponentType; - setLimitReqState?: (state: LimitRequested) => void; - }; +type PrometheusUtilizationItemProps = PrometheusCommonProps & { + utilizationQuery: string; + totalQuery?: string; + limitQuery?: string; + requestQuery?: string; + TopConsumerPopover?: ComponentType; + setLimitReqState?: (state: LimitRequested) => void; +}; -type PrometheusMultilineUtilizationItemProps = DashboardItemProps & - PrometheusCommonProps & { - queries: QueryWithDescription[]; - TopConsumerPopovers?: ComponentType[]; - }; +type PrometheusMultilineUtilizationItemProps = PrometheusCommonProps & { + queries: QueryWithDescription[]; + TopConsumerPopovers?: ComponentType[]; +}; type UtilizationCardNodeFilterProps = { machineConfigPools: MachineConfigPoolKind[]; diff --git a/frontend/public/components/dashboard/project-dashboard/activity-card.tsx b/frontend/public/components/dashboard/project-dashboard/activity-card.tsx index 36b6de721d2..d0f88413c58 100644 --- a/frontend/public/components/dashboard/project-dashboard/activity-card.tsx +++ b/frontend/public/components/dashboard/project-dashboard/activity-card.tsx @@ -8,8 +8,9 @@ import ActivityBody, { RecentEventsBody, OngoingActivityBody, } from '@console/shared/src/components/dashboard/activity-card/ActivityBody'; -import { DashboardItemProps, withDashboardResources } from '../with-dashboard-resources'; -import type { FirehoseResource, FirehoseResult } from '../../utils/types'; +import { useDynamicK8sWatchResources } from '@console/shared/src/hooks/useDynamicK8sWatchResources'; +import { useK8sWatchResource } from '../../utils/k8s-watch-hook'; +import type { FirehoseResult } from '../../utils/types'; import { EventModel } from '../../../models'; import { EventKind, K8sKind } from '../../../module/k8s'; import { @@ -24,151 +25,124 @@ import { getName } from '@console/shared/src/selectors/common'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom-v5-compat'; -const getEventsResource = (projectName: string): FirehoseResource => ({ - isList: true, - kind: EventModel.kind, - prop: 'events', - namespace: projectName, -}); +const RecentEvent: FC<{ projectName: string; viewEvents: string }> = ({ + projectName, + viewEvents, +}) => { + const { t } = useTranslation(); + + const [eventsData, eventsLoaded, eventsLoadError] = useK8sWatchResource( + projectName + ? { + isList: true, + kind: EventModel.kind, + namespace: projectName, + } + : null, + ); + + const events: FirehoseResult = useMemo( + () => ({ + data: eventsData, + loaded: eventsLoaded, + loadError: eventsLoadError, + }), + [eventsData, eventsLoaded, eventsLoadError], + ); -const RecentEvent = withDashboardResources( - ({ watchK8sResource, stopWatchK8sResource, resources, projectName, viewEvents }) => { - useEffect(() => { - if (projectName) { - const eventsResource = getEventsResource(projectName); - watchK8sResource(eventsResource); - return () => { - stopWatchK8sResource(eventsResource); - }; - } - }, [watchK8sResource, stopWatchK8sResource, projectName]); - return ( - } - moreLink={viewEvents} - /> - ); - }, -); + const shouldShowFooter = events?.loaded && events?.data && events.data.length > 50; + + return ( + <> + + {shouldShowFooter && ( + <> + + + + {t('public~View all events')} + + + + )} + + ); +}; const mapStateToProps = (state: RootState): OngoingActivityReduxProps => ({ models: state.k8s.getIn(['RESOURCES', 'models']) as ImmutableMap, }); -const OngoingActivity = connect(mapStateToProps)( - withDashboardResources( - ({ - watchK8sResource, - stopWatchK8sResource, - resources, - projectName, - models, - }: DashboardItemProps & OngoingActivityProps) => { - const [resourceActivityExtensions] = useResolvedExtensions< - DashboardsOverviewResourceActivity - >(isDashboardsOverviewResourceActivity); - - const resourceActivities = useMemo( - () => - resourceActivityExtensions.filter((e) => { - const model = models.get(e.properties.k8sResource.kind); - return model && model.namespaced; - }), - [resourceActivityExtensions, models], - ); - - useEffect(() => { - if (projectName) { - resourceActivities.forEach((a, index) => { - watchK8sResource( - uniqueResource({ ...a.properties.k8sResource, namespace: projectName }, index), - ); - }); - return () => { - resourceActivities.forEach((a, index) => { - stopWatchK8sResource(uniqueResource(a.properties.k8sResource, index)); - }); - }; - } - }, [watchK8sResource, stopWatchK8sResource, projectName, resourceActivities]); - - const allResourceActivities = useMemo( - () => - _.flatten( - resourceActivities.map((a, index) => { - const k8sResources = _.get( - resources, - [uniqueResource(a.properties.k8sResource, index).prop, 'data'], - [], - ) as FirehoseResult['data']; - return k8sResources - .filter((r) => (a.properties.isActivity ? a.properties.isActivity(r) : true)) - .map((r) => ({ - resource: r, - timestamp: a.properties.getTimestamp ? a.properties.getTimestamp(r) : null, - component: a.properties.component, - })); - }), - ), - [resourceActivities, resources], - ); - - const resourcesLoaded = useMemo( - () => - resourceActivities.every((a, index) => - _.get(resources, [uniqueResource(a.properties.k8sResource, index).prop, 'loaded']), - ), - [resourceActivities, resources], - ); - - return ( - - ); - }, - ), -); - -const RecentEventFooter = withDashboardResources( - ({ - watchK8sResource, - stopWatchK8sResource, - resources, - projectName, - viewEvents, - }: DashboardItemProps & { projectName: string; viewEvents: string }) => { - const { t } = useTranslation(); - useEffect(() => { - if (projectName) { - const eventsResource = getEventsResource(projectName); - watchK8sResource(eventsResource); - return () => { - stopWatchK8sResource(eventsResource); - }; - } - }, [watchK8sResource, stopWatchK8sResource, projectName]); - - const events = resources.events as FirehoseResult; - const shouldShowFooter = events?.loaded && events?.data && events.data.length > 50; - - if (!shouldShowFooter) { - return null; +const OngoingActivityComponent: FC = ({ projectName, models }) => { + const { watchResource, stopWatchResource, results: resources } = useDynamicK8sWatchResources(); + + const [resourceActivityExtensions] = useResolvedExtensions( + isDashboardsOverviewResourceActivity, + ); + + const resourceActivities = useMemo( + () => + resourceActivityExtensions.filter((e) => { + const model = models.get(e.properties.k8sResource.kind); + return model && model.namespaced; + }), + [resourceActivityExtensions, models], + ); + + useEffect(() => { + if (projectName) { + resourceActivities.forEach((a, index) => { + const uniqueRes = uniqueResource(a.properties.k8sResource, index); + const { prop, ...resourceConfig } = uniqueRes; + watchResource(prop, { ...resourceConfig, namespace: projectName }); + }); + return () => { + resourceActivities.forEach((a, index) => { + const resourceKey = uniqueResource(a.properties.k8sResource, index).prop; + stopWatchResource(resourceKey); + }); + }; } + }, [watchResource, stopWatchResource, projectName, resourceActivities]); + + const allResourceActivities = useMemo( + () => + _.flatten( + resourceActivities.map((a, index) => { + const k8sResources = _.get( + resources, + [uniqueResource(a.properties.k8sResource, index).prop, 'data'], + [], + ) as FirehoseResult['data']; + return k8sResources + .filter((r) => (a.properties.isActivity ? a.properties.isActivity(r) : true)) + .map((r) => ({ + resource: r, + timestamp: a.properties.getTimestamp ? a.properties.getTimestamp(r) : null, + component: a.properties.component, + })); + }), + ), + [resourceActivities, resources], + ); + + const resourcesLoaded = useMemo( + () => + resourceActivities.every((a, index) => + _.get(resources, [uniqueResource(a.properties.k8sResource, index).prop, 'loaded']), + ), + [resourceActivities, resources], + ); - return ( - <> - - - - {t('public~View all events')} - - - - ); - }, -); + return ( + + ); +}; + +const OngoingActivity = connect(mapStateToProps)(OngoingActivityComponent); export const ActivityCard: FC = () => { const { obj } = useContext(ProjectDashboardContext); @@ -185,17 +159,11 @@ export const ActivityCard: FC = () => { - ); }; -type RecentEventProps = DashboardItemProps & { - projectName: string; - viewEvents: string; -}; - type OngoingActivityReduxProps = { models: ImmutableMap; }; diff --git a/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx b/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx index e5eba32ade5..99bdaf4ebb5 100644 --- a/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx +++ b/frontend/public/components/dashboard/project-dashboard/inventory-card.tsx @@ -1,7 +1,7 @@ import { useEffect, useContext } from 'react'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { DashboardItemProps, withDashboardResources } from '../with-dashboard-resources'; +import { useDynamicK8sWatchResources } from '@console/shared/src/hooks/useDynamicK8sWatchResources'; import { Card, CardBody, CardHeader, CardTitle, Stack, StackItem } from '@patternfly/react-core'; import { PodModel, @@ -48,79 +48,79 @@ const createFirehoseResource = (model: K8sKind, projectName: string): FirehoseRe namespace: projectName, }); -const ProjectInventoryItem = withDashboardResources( - ({ - projectName, - watchK8sResource, - stopWatchK8sResource, - resources, - model, - mapper, - additionalResources, - additionalDynamicResources, - }: ProjectInventoryItemProps) => { - useEffect(() => { - if (projectName) { - const resource = createFirehoseResource(model, projectName); - watchK8sResource(resource); +const ProjectInventoryItem: React.FC = ({ + projectName, + model, + mapper, + additionalResources, + additionalDynamicResources, +}) => { + const { watchResource, stopWatchResource, results: resources } = useDynamicK8sWatchResources(); + + useEffect(() => { + if (projectName) { + const resource = createFirehoseResource(model, projectName); + const { prop, ...resourceConfig } = resource; + watchResource(prop, resourceConfig); + if (additionalResources) { + additionalResources.forEach((r) => { + const { prop: key, ...config } = { ...r, namespace: projectName }; + watchResource(key, config); + }); + } + return () => { + stopWatchResource(resource.prop); if (additionalResources) { - additionalResources.forEach((r) => watchK8sResource({ ...r, namespace: projectName })); + additionalResources.forEach((r) => stopWatchResource(r.prop)); } - return () => { - stopWatchK8sResource(resource); - if (additionalResources) { - additionalResources.forEach(stopWatchK8sResource); - } - }; - } - }, [watchK8sResource, stopWatchK8sResource, projectName, model, additionalResources]); + }; + } + }, [watchResource, stopWatchResource, projectName, model, additionalResources]); - const resourceData = _.get(resources.resource, 'data', []) as FirehoseResult['data']; - const resourceLoaded = _.get(resources.resource, 'loaded'); - const resourceLoadError = _.get(resources.resource, 'loadError'); + const resourceData = _.get(resources.resource, 'data', []) as FirehoseResult['data']; + const resourceLoaded = _.get(resources.resource, 'loaded'); + const resourceLoadError = _.get(resources.resource, 'loadError'); - const additionalResourcesData = additionalResources - ? additionalResources.reduce((acc, r) => { - acc[r.prop] = _.get(resources[r.prop], 'data'); - return acc; - }, {}) - : {}; - const additionalResourcesLoaded = additionalResources - ? additionalResources - .filter((r) => !r.optional) - .every((r) => _.get(resources[r.prop], 'loaded')) - : true; - const additionalResourcesLoadError = additionalResources - ? additionalResources - .filter((r) => !r.optional) - .some((r) => !!_.get(resources[r.prop], 'loadError')) - : false; + const additionalResourcesData = additionalResources + ? additionalResources.reduce((acc, r) => { + acc[r.prop] = _.get(resources[r.prop], 'data'); + return acc; + }, {}) + : {}; + const additionalResourcesLoaded = additionalResources + ? additionalResources + .filter((r) => !r.optional) + .every((r) => _.get(resources[r.prop], 'loaded')) + : true; + const additionalResourcesLoadError = additionalResources + ? additionalResources + .filter((r) => !r.optional) + .some((r) => !!_.get(resources[r.prop], 'loadError')) + : false; - const dynamicResources = useK8sWatchResources(additionalDynamicResources || {}); - const dynamicResourcesError = Object.values(dynamicResources).find((r) => r.loadError) - ?.loadError; - const dynamicResourcesLoaded = Object.keys(dynamicResources).every( - (key) => dynamicResources[key].loaded, - ); + const dynamicResources = useK8sWatchResources(additionalDynamicResources || {}); + const dynamicResourcesError = Object.values(dynamicResources).find((r) => r.loadError)?.loadError; + const dynamicResourcesLoaded = Object.keys(dynamicResources).every( + (key) => dynamicResources[key].loaded, + ); - return ( - - - - ); - }, -); + return ( + + + + ); +}; export const InventoryCard = () => { const [itemExtensions] = useResolvedExtensions( @@ -191,7 +191,7 @@ export const InventoryCard = () => { ); }; -type ProjectInventoryItemProps = DashboardItemProps & { +type ProjectInventoryItemProps = { projectName: string; model: K8sKind; mapper?: StatusGroupMapper; diff --git a/frontend/public/components/dashboard/with-dashboard-resources.tsx b/frontend/public/components/dashboard/with-dashboard-resources.tsx deleted file mode 100644 index 1e73188f4c0..00000000000 --- a/frontend/public/components/dashboard/with-dashboard-resources.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import type { ComponentType } from 'react'; -import { Component } from 'react'; -import { connect } from 'react-redux'; -import * as _ from 'lodash'; - -import { RESULTS_TYPE } from '../../reducers/dashboard-results'; -import { NotificationAlerts } from '../../reducers/observe'; -import { - StopWatchPrometheusAction, - stopWatchPrometheusQuery, - stopWatchURL, - StopWatchURLAction, - watchPrometheusQuery, - WatchPrometheusQueryAction, - watchURL, - WatchURLAction, - getQueryKey, -} from '../../actions/dashboards'; -import { RootState } from '../../redux'; -import { Firehose } from '../utils/firehose'; -import type { FirehoseResource, FirehoseResult } from '../utils/types'; -import { K8sResourceKind, AppliedClusterResourceQuotaKind } from '../../module/k8s'; -import { PrometheusResponse } from '../graphs'; -import { Fetch, RequestMap } from '@console/dynamic-plugin-sdk/src/api/internal-types'; - -const mapDispatchToProps: DispatchToProps = (dispatch) => ({ - watchURL: (url, fetch) => dispatch(watchURL(url, fetch)), - stopWatchURL: (url) => dispatch(stopWatchURL(url)), - watchPrometheusQuery: (query, namespace, timespan) => - dispatch(watchPrometheusQuery(query, namespace, timespan)), - stopWatchPrometheusQuery: (query, timespan) => - dispatch(stopWatchPrometheusQuery(query, timespan)), -}); - -const mapStateToProps = (state: RootState) => ({ - [RESULTS_TYPE.URL]: state.dashboards.get(RESULTS_TYPE.URL), - [RESULTS_TYPE.PROMETHEUS]: state.dashboards.get(RESULTS_TYPE.PROMETHEUS) as RequestMap< - PrometheusResponse - >, - notificationAlerts: state.observe.get('notificationAlerts'), -}); - -type StateProps = ReturnType; -type DispatchProps = ReturnType; - -export const withDashboardResources =

    ( - WrappedComponent: ComponentType

    , -) => - connect>( - mapStateToProps, - mapDispatchToProps, - )( - class WithDashboardResources extends Component< - WithDashboardResourcesProps, - WithDashboardResourcesState - > { - private urls: Array = []; - private queries: Array = []; - private watchingAlerts: boolean = false; - - constructor(props) { - super(props); - this.state = { - k8sResources: [], - }; - } - - shouldComponentUpdate( - nextProps: WithDashboardResourcesProps, - nextState: WithDashboardResourcesState, - ) { - const urlResultChanged = this.urls.some( - (urlKey) => - this.props[RESULTS_TYPE.URL].getIn([urlKey, 'data']) !== - nextProps[RESULTS_TYPE.URL].getIn([urlKey, 'data']) || - this.props[RESULTS_TYPE.URL].getIn([urlKey, 'loadError']) !== - nextProps[RESULTS_TYPE.URL].getIn([urlKey, 'loadError']), - ); - const queryResultChanged = this.queries.some( - (query) => - this.props[RESULTS_TYPE.PROMETHEUS].getIn([query, 'data']) !== - nextProps[RESULTS_TYPE.PROMETHEUS].getIn([query, 'data']) || - this.props[RESULTS_TYPE.PROMETHEUS].getIn([query, 'loadError']) !== - nextProps[RESULTS_TYPE.PROMETHEUS].getIn([query, 'loadError']), - ); - const alertsResultChanged = - this.props?.notificationAlerts?.data !== nextProps?.notificationAlerts?.data || - this.props?.notificationAlerts?.loadError !== nextProps?.notificationAlerts?.loadError; - const k8sResourcesChanged = this.state.k8sResources !== nextState.k8sResources; - - const nextExternalProps = this.getExternalProps(nextProps); - const externalProps = this.getExternalProps(this.props); - - return ( - urlResultChanged || - queryResultChanged || - k8sResourcesChanged || - (this.watchingAlerts && alertsResultChanged) || - Object.keys(nextExternalProps).length !== Object.keys(externalProps).length || - Object.keys(nextExternalProps).some( - (key) => nextExternalProps[key] !== externalProps[key], - ) - ); - } - - watchURL: WatchURL = (url, fetch) => { - this.urls.push(url); - this.props.watchURL(url, fetch); - }; - - watchPrometheus: WatchPrometheus = (query, namespace, timespan) => { - this.queries.push(getQueryKey(query, timespan)); - this.props.watchPrometheusQuery(query, namespace, timespan); - }; - - stopWatchPrometheusQuery: StopWatchPrometheus = (query, timespan) => { - const queryKey = getQueryKey(query, timespan); - this.queries = this.queries.filter((q) => q !== queryKey); - this.props.stopWatchPrometheusQuery(query, timespan); - }; - - watchAlerts: WatchAlerts = () => { - this.watchingAlerts = true; - }; - - stopWatchAlerts: StopWatchAlerts = () => { - this.watchingAlerts = false; - }; - - watchK8sResource: WatchK8sResource = (resource) => { - this.setState((state: WithDashboardResourcesState) => ({ - k8sResources: [...state.k8sResources, { ...resource, optional: true }], - })); - }; - - stopWatchK8sResource: StopWatchK8sResource = (resource) => { - this.setState((state: WithDashboardResourcesState) => ({ - k8sResources: state.k8sResources.filter((r) => r.prop !== resource.prop), - })); - }; - - getExternalProps = (props) => { - return _.omit( - props, - 'watchURL', - 'stopWatchURL', - 'watchPrometheusQuery', - 'stopWatchPrometheusQuery', - 'watchAlerts', - 'stopWatchAlerts', - RESULTS_TYPE.URL, - RESULTS_TYPE.PROMETHEUS, - 'notificationAlerts', - ); - }; - - render() { - return ( - - - - ); - } - }, - ); - -type DispatchToProps = ( - dispatch: any, -) => { - watchURL: WatchURL; - stopWatchURL: StopWatchURL; - watchPrometheusQuery: WatchPrometheus; - stopWatchPrometheusQuery: StopWatchPrometheus; -}; - -type WatchURL = (url: string, fetch?: Fetch) => void; -type StopWatchURL = (url: string) => void; -type WatchPrometheus = (query: string, namespace?: string, timespan?: number) => void; -type StopWatchPrometheus = (query: string, timespan?: number) => void; -type WatchAlerts = () => void; -type StopWatchAlerts = () => void; - -type WithDashboardResourcesState = { - k8sResources: FirehoseResource[]; -}; - -type WithDashboardResourcesProps = { - watchURL: WatchURLAction; - watchPrometheusQuery: WatchPrometheusQueryAction; - stopWatchURL: StopWatchURLAction; - stopWatchPrometheusQuery: StopWatchPrometheusAction; - [RESULTS_TYPE.PROMETHEUS]: RequestMap; - [RESULTS_TYPE.URL]: RequestMap; - notificationAlerts: any; -}; - -export type WatchK8sResource = (resource: FirehoseResource) => void; -export type StopWatchK8sResource = (resource: FirehoseResource) => void; - -export type DashboardItemProps = { - watchURL: WatchURL; - stopWatchURL: StopWatchURL; - watchPrometheus: WatchPrometheus; - stopWatchPrometheusQuery: StopWatchPrometheus; - watchAlerts: WatchAlerts; - stopWatchAlerts: StopWatchAlerts; - urlResults: RequestMap; - prometheusResults: RequestMap; - notificationAlerts: NotificationAlerts; - watchK8sResource: WatchK8sResource; - stopWatchK8sResource: StopWatchK8sResource; - resources?: { - [key: string]: - | FirehoseResult - | FirehoseResult - | FirehoseResult; - }; -};