From 5a336769b30497ae499d7d69dc4dc41e65bd5664 Mon Sep 17 00:00:00 2001 From: Montse Ortega Date: Wed, 22 Jan 2025 12:30:53 +0100 Subject: [PATCH] ECOPROJECT-2540: List of reports are constantly changing (#74) * List of reports are constantly changing Signed-off-by: Montse Ortega * List of reports changing all the time Signed-off-by: Montse Ortega --------- Signed-off-by: Montse Ortega --- .../@types/DiscoverySources.d.ts | 3 +- .../contexts/discovery-sources/Provider.tsx | 41 ++- .../steps/connect/ConnectStep.tsx | 11 +- .../steps/connect/sources-table/Constants.ts | 2 +- .../connect/sources-table/SourcesTable.tsx | 296 +++++++++++------- .../sources-table/empty-state/EmptyState.tsx | 1 - 6 files changed, 218 insertions(+), 136 deletions(-) diff --git a/apps/demo/src/migration-wizard/contexts/discovery-sources/@types/DiscoverySources.d.ts b/apps/demo/src/migration-wizard/contexts/discovery-sources/@types/DiscoverySources.d.ts index 67a6b37..b1dc7ff 100644 --- a/apps/demo/src/migration-wizard/contexts/discovery-sources/@types/DiscoverySources.d.ts +++ b/apps/demo/src/migration-wizard/contexts/discovery-sources/@types/DiscoverySources.d.ts @@ -18,12 +18,13 @@ declare namespace DiscoverySources { agents: Agent[]|undefined; isLoadingAgents: boolean; errorLoadingAgents?: Error; - listAgents: () => Promise; + listAgents: () => Promise; deleteAgent: (agent: Agent) => Promise; isDeletingAgent: boolean; errorDeletingAgent?: Error; selectAgent: (agent:Agent) => void; agentSelected: Agent; selectSourceById: (sourceId:string)=>void; + getSourceById: (sourceId: string) => Source; }; } diff --git a/apps/demo/src/migration-wizard/contexts/discovery-sources/Provider.tsx b/apps/demo/src/migration-wizard/contexts/discovery-sources/Provider.tsx index 3b0022e..cb1bc64 100644 --- a/apps/demo/src/migration-wizard/contexts/discovery-sources/Provider.tsx +++ b/apps/demo/src/migration-wizard/contexts/discovery-sources/Provider.tsx @@ -17,16 +17,20 @@ export const Provider: React.FC = (props) => { const [agentSelected, setAgentSelected] = useState(null) + const [sourcesLoaded, setSourcesLoaded] = useState(false); + const sourceApi = useInjection(Symbols.SourceApi); const agentsApi = useInjection(Symbols.AgentApi); const [listAgentsState, listAgents] = useAsyncFn(async () => { + if (!sourcesLoaded) return; const agents = await agentsApi.listAgents(); return agents; - }); + },[sourcesLoaded]); const [listSourcesState, listSources] = useAsyncFn(async () => { const sources = await sourceApi.listSources(); + setSourcesLoaded(true); return sources; }); @@ -82,9 +86,15 @@ export const Provider: React.FC = (props) => { setIsPolling(false); } }, [isPolling]); + useInterval(() => { - listSources(); - listAgents(); + if (!listSourcesState.loading){ + listSources().then(() => { + if (sourcesLoaded) { + listAgents(); + } + }); + } }, pollingDelay); const selectSource = useCallback((source: Source|null) => { @@ -92,13 +102,22 @@ export const Provider: React.FC = (props) => { }, []); const selectSourceById = useCallback((sourceId: string) => { - const source = listSourcesState.value?.find(source => source.id === sourceId); - setSourceSelected(source||null); - }, [listSourcesState.value]); + if (!listSourcesState.loading){ + const source = listSourcesState.value?.find(source => source.id === sourceId); + setSourceSelected(source||null); + } + else { + listSources().then((_sources)=>{ + const source = _sources.find(source => source.id === sourceId); + setSourceSelected(source||null); + }) + } + + }, [listSources, listSourcesState]); const selectAgent = useCallback(async (agent: Agent) => { - setAgentSelected(agent); + setAgentSelected(agent); if (agent && agent.sourceId!==null) await selectSourceById(agent.sourceId ?? ''); }, [selectSourceById]); @@ -111,6 +130,11 @@ export const Provider: React.FC = (props) => { const deletedAgent = await agentsApi.deleteAgent({id: agent.id}); return deletedAgent; }); + + const getSourceById = useCallback((sourceId: string) => { + const source = listSourcesState.value?.find(source => source.id === sourceId); + return source; + }, [listSourcesState.value]); const ctx: DiscoverySources.Context = { sources: listSourcesState.value ?? [], @@ -137,7 +161,8 @@ export const Provider: React.FC = (props) => { errorDeletingAgent: deleteAgentState.error, selectAgent, agentSelected: agentSelected, - selectSourceById + selectSourceById, + getSourceById }; return {children}; diff --git a/apps/demo/src/migration-wizard/steps/connect/ConnectStep.tsx b/apps/demo/src/migration-wizard/steps/connect/ConnectStep.tsx index acdae01..72f0f34 100644 --- a/apps/demo/src/migration-wizard/steps/connect/ConnectStep.tsx +++ b/apps/demo/src/migration-wizard/steps/connect/ConnectStep.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useState } from "react"; import { Stack, StackItem, @@ -31,14 +31,6 @@ export const ConnectStep: React.FC = () => { setShouldShowDiscoverySetupModal((lastState) => !lastState); }, []); const hasAgents = discoverySourcesContext.agents && discoverySourcesContext.agents.length > 0; - const [firstAgent, ..._otherAgents] = discoverySourcesContext.agents || []; - - useEffect(() => { - if (!discoverySourcesContext.agentSelected && firstAgent) { - discoverySourcesContext.selectAgent(firstAgent); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [firstAgent]); return ( @@ -110,7 +102,6 @@ export const ConnectStep: React.FC = () => { const sshKey = form["discoverySourceSshKey"].value as string; await discoverySourcesContext.downloadSource(sshKey); toggleDiscoverySourceSetupModal(); - //await discoverySourcesContext.listSources(); await discoverySourcesContext.listAgents(); }} /> diff --git a/apps/demo/src/migration-wizard/steps/connect/sources-table/Constants.ts b/apps/demo/src/migration-wizard/steps/connect/sources-table/Constants.ts index 8207149..0a054df 100644 --- a/apps/demo/src/migration-wizard/steps/connect/sources-table/Constants.ts +++ b/apps/demo/src/migration-wizard/steps/connect/sources-table/Constants.ts @@ -1,4 +1,4 @@ import { Time } from "#/common/Time"; export const VALUE_NOT_AVAILABLE = "-"; -export const DEFAULT_POLLING_DELAY = 3 * Time.Second; +export const DEFAULT_POLLING_DELAY = 1 * Time.Second; diff --git a/apps/demo/src/migration-wizard/steps/connect/sources-table/SourcesTable.tsx b/apps/demo/src/migration-wizard/steps/connect/sources-table/SourcesTable.tsx index 470d0c1..167debc 100644 --- a/apps/demo/src/migration-wizard/steps/connect/sources-table/SourcesTable.tsx +++ b/apps/demo/src/migration-wizard/steps/connect/sources-table/SourcesTable.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { useMount, useUnmount } from "react-use"; import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; import { EmptyState } from "./empty-state/EmptyState"; @@ -9,19 +9,43 @@ import { SourceStatusView } from "./SourceStatusView"; import { useDiscoverySources } from "#/migration-wizard/contexts/discovery-sources/Context"; import { Radio, Spinner } from "@patternfly/react-core"; import { Link } from "react-router-dom"; +import { Agent, Source } from "@migration-planner-ui/api-client/models"; export const SourcesTable: React.FC = () => { const discoverySourcesContext = useDiscoverySources(); - const hasAgents = discoverySourcesContext.agents && discoverySourcesContext.agents.length > 0; - const [firstAgent, ..._otherAgents] = discoverySourcesContext.agents ?? []; + const prevAgentsRef = useRef([]); + const [isLoading, setIsLoading] = useState(true); + const [relatedSources, setRelatedSources] = useState>({}); // Mapping between agentId -> source + const timeoutRef = useRef(null); + + // Memorize ordered agents + const memoizedAgents = useMemo(() => { + const areAgentsEqual = (prevAgents: typeof discoverySourcesContext.agents, newAgents: typeof discoverySourcesContext.agents): boolean => { + if (!prevAgents || !newAgents || prevAgents.length !== newAgents.length) return false; + return prevAgents.every((agent, index) => agent.id === newAgents[index].id); + }; + + if (!areAgentsEqual(prevAgentsRef.current, discoverySourcesContext.agents)) { + prevAgentsRef.current = discoverySourcesContext.agents; + return discoverySourcesContext.agents + ? discoverySourcesContext.agents.sort((a: Agent, b: Agent) => a.id.localeCompare(b.id)) + : []; + } + return prevAgentsRef.current; + }, [discoverySourcesContext]); + + const [firstAgent, ..._otherAgents] = memoizedAgents ?? []; + const hasAgents = memoizedAgents && memoizedAgents.length>0; useMount(async () => { + discoverySourcesContext.startPolling(DEFAULT_POLLING_DELAY); if (!discoverySourcesContext.isPolling) { - await Promise.all([ - discoverySourcesContext.listSources(), - discoverySourcesContext.listAgents() - ]); - } + await Promise.all([ + discoverySourcesContext.listSources(), + discoverySourcesContext.listAgents() + ]); + } + }); useUnmount(() => { @@ -29,116 +53,158 @@ export const SourcesTable: React.FC = () => { }); useEffect(() => { - if ( - ["error", "up-to-date"].includes( - discoverySourcesContext.agentSelected?.status - ) - ) { - discoverySourcesContext.stopPolling(); - return; - } else { - discoverySourcesContext.startPolling(DEFAULT_POLLING_DELAY); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [discoverySourcesContext.agentSelected?.status]); + // Use timeout to verify memoizedAgents variable + timeoutRef.current = setTimeout(() => { + if (memoizedAgents && memoizedAgents.length === 0) { + setIsLoading(false); + } + }, 3000); // Timeout in milisecons (3 seconds here) - if ( - (discoverySourcesContext.agentSelected === undefined || - discoverySourcesContext.sourceSelected === undefined) && - !(discoverySourcesContext.agentSelected?.length === 0 || - discoverySourcesContext.sourceSelected?.length === 0) - ) { - return ; // Loading agent and source - } - return ( - - {hasAgents && ( - - - - - - - - - - - - )} - - {hasAgents ? ( - discoverySourcesContext.agents && discoverySourcesContext.agents.map((agent) => { - const source = discoverySourcesContext.sourceSelected; - return( - - - - - - - - - - - )}) - ) : ( + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + return () => { + // Clean the timeout in case unmount the component + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [memoizedAgents]); + + + // Load the sources related to each agent + useEffect(() => { + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const fetchRelatedSources = async () => { + if (memoizedAgents && memoizedAgents.length > 0) { + const sourcesMap: Record = {}; + for (const agent of memoizedAgents) { + if (agent.sourceId) { + const source = await discoverySourcesContext.getSourceById(agent.sourceId); + sourcesMap[agent.id] = source ?? null; + } + } + setRelatedSources(sourcesMap); + } + }; + + + if (hasAgents) { + fetchRelatedSources().finally(() =>{ + if (!discoverySourcesContext.agentSelected) { + discoverySourcesContext.selectAgent(firstAgent); + } + setIsLoading(false); + }); + } + + }, [memoizedAgents, hasAgents, discoverySourcesContext, firstAgent]); + + + console.log("---------"); + console.log(hasAgents); + console.log("----------"); + // Show spinner until all data is loaded + if ((isLoading) ) { + return ( +
{Columns.CredentialsUrl}{Columns.Status}{Columns.Hosts}{Columns.VMs}{Columns.Networks}{Columns.Datastores}{Columns.Actions}
- {" "} - - {agent.credentialUrl} - - ) : ( - agent.credentialUrl - ) - } - isChecked={ - discoverySourcesContext.agentSelected - ? discoverySourcesContext.agentSelected?.id === agent.id - : firstAgent.id === agent.id - } - onChange={() => discoverySourcesContext.selectAgent(agent)} - /> - - - - {source!==null && source.inventory?.infra.totalHosts || VALUE_NOT_AVAILABLE} - - {source!==null && source.inventory?.vms.total || VALUE_NOT_AVAILABLE} - - {((source!==null && source.inventory?.infra.networks) ?? []).length || - VALUE_NOT_AVAILABLE} - - {((source!==null && source.inventory?.infra.datastores) ?? []).length || - VALUE_NOT_AVAILABLE} - - {agent.credentialUrl !== "Example report" && ( { - event.stopPropagation(); - await discoverySourcesContext.deleteAgent(agent); - event.dismissConfirmationModal(); - await discoverySourcesContext.listAgents(); - await discoverySourcesContext.listSources(); - }} - />)} -
+ - + +
- + +
+ ); + } + else { + return ( + + {memoizedAgents && memoizedAgents.length>0 && ( + + + + + + + + + + + )} - -
{Columns.CredentialsUrl}{Columns.Status}{Columns.Hosts}{Columns.VMs}{Columns.Networks}{Columns.Datastores}{Columns.Actions}
- ); + + {memoizedAgents && memoizedAgents.length>0 ? ( + memoizedAgents.map((agent) => { + const source = relatedSources[agent.id]; // Get the source related to this agent + return ( + + + + {agent.credentialUrl} + + ) : ( + agent.credentialUrl + ) + } + isChecked={ + discoverySourcesContext.agentSelected + ? discoverySourcesContext.agentSelected.id === agent.id + : false + } + onChange={() => discoverySourcesContext.selectAgent(agent)} + /> + + + + + + {(source?.inventory?.infra.totalHosts ?? VALUE_NOT_AVAILABLE)} + + + {(source?.inventory?.vms.total ?? VALUE_NOT_AVAILABLE)} + + + {(source?.inventory?.infra.networks?.length ?? VALUE_NOT_AVAILABLE)} + + + {(source?.inventory?.infra.datastores?.length ?? VALUE_NOT_AVAILABLE)} + + + {agent.credentialUrl !== "Example report" && ( + { + event.stopPropagation(); + await discoverySourcesContext.deleteAgent(agent); + event.dismissConfirmationModal(); + await Promise.all([ + discoverySourcesContext.listAgents(), + discoverySourcesContext.listSources(), + ]); + }} + /> + )} + + + ); + }) + ) : ( + + + + + + )} + + + ); + } + }; - -SourcesTable.displayName = "SourcesTable"; diff --git a/apps/demo/src/migration-wizard/steps/connect/sources-table/empty-state/EmptyState.tsx b/apps/demo/src/migration-wizard/steps/connect/sources-table/empty-state/EmptyState.tsx index 0c28458..bfbe0ba 100644 --- a/apps/demo/src/migration-wizard/steps/connect/sources-table/empty-state/EmptyState.tsx +++ b/apps/demo/src/migration-wizard/steps/connect/sources-table/empty-state/EmptyState.tsx @@ -27,7 +27,6 @@ export const EmptyState: React.FC = () => { const handleTryAgain = useCallback(() => { if (!discoverySourcesContext.isLoadingAgents) { - //discoverySourcesContext.listSources(); discoverySourcesContext.listAgents(); } }, [discoverySourcesContext]);