diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx index cfdcd68d..4ace6a63 100644 --- a/frontend/src/components/TopBar.tsx +++ b/frontend/src/components/TopBar.tsx @@ -17,6 +17,7 @@ import { LoadInitialState} from '../model' // UI Elements import { SideBarIcons } from './SideBar'; import TopNavigation from "@awsui/components-react/top-navigation"; +import { useQueryClient } from 'react-query'; function regions(selected: any) { let supportedRegions = [ @@ -90,6 +91,7 @@ function regions(selected: any) { export default function Topbar(props: any) { let username = useState(['identity', 'attributes', 'email']); + const queryClient = useQueryClient(); const defaultRegion = useState(['aws', 'region']) || "DEFAULT"; const region = useState(['app', 'selectedRegion']) || defaultRegion; @@ -102,6 +104,7 @@ export default function Topbar(props: any) { let newRegion = region.detail.id; setState(['app', 'selectedRegion'], newRegion); LoadInitialState(); + queryClient.invalidateQueries(); } const profileActions = [ diff --git a/frontend/src/old-pages/Clusters/Clusters.tsx b/frontend/src/old-pages/Clusters/Clusters.tsx index 9b8a78ed..f2e9dc72 100644 --- a/frontend/src/old-pages/Clusters/Clusters.tsx +++ b/frontend/src/old-pages/Clusters/Clusters.tsx @@ -7,10 +7,10 @@ // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and // limitations under the License. -import React from 'react'; +import React, { useEffect } from 'react'; import { NavigateFunction, useNavigate, useParams } from "react-router-dom" import { ListClusters } from '../../model' -import { useState, getState, clearState, setState, isAdmin } from '../../store' +import { useState, clearState, setState, isAdmin } from '../../store' import { selectCluster } from './util' import { findFirst } from '../../util' import { useTranslation } from 'react-i18next'; @@ -34,7 +34,6 @@ import Details from "./Details"; import { wizardShow } from '../Configure/Configure'; import AddIcon from '@mui/icons-material/Add'; - export interface Cluster { cloudformationStackArn: string, cloudformationStackStatus: string, @@ -44,44 +43,32 @@ export interface Cluster { version: string } -async function updateClusterList(navigate: NavigateFunction) { - const selectedClusterName = getState(['app', 'clusters', 'selected']); - const oldStatus = getState(['app', 'clusters', 'selectedStatus']); - - try { - const clusterList = await ListClusters(); - if(selectedClusterName) { - const selectedCluster = findFirst(clusterList, (c: Cluster) => c.clusterName === selectedClusterName); - if(selectedCluster) { - if(oldStatus !== selectedCluster.clusterStatus) { - setState(['app', 'clusters', 'selectedStatus'], selectedCluster.clusterStatus); - } - if((oldStatus === 'CREATE_IN_PROGRESS' && selectedCluster.clusterStatus === 'CREATE_COMPLETE') || (oldStatus === 'UPDATE_IN_PROGRESS' && selectedCluster.clusterStatus === 'UPDATE_COMPLETE')) { - selectCluster(selectedClusterName, null); - } else if (oldStatus === 'DELETE_IN_PROGRESS') { - clearState(['app', 'clusters', 'selected']); - navigate('/clusters'); - } - } - } - } catch (error) {} -} - type ClusterListProps = { clusters: Cluster[] } +export function onClustersUpdate(selectedClusterName:string, clusters: Cluster[], oldStatus: string, navigate: NavigateFunction): void { + if(!selectedClusterName) { + return; + } + const selectedCluster = findFirst(clusters, (c: Cluster) => c.clusterName === selectedClusterName); + if(selectedCluster) { + if(oldStatus !== selectedCluster.clusterStatus) { + setState(['app', 'clusters', 'selectedStatus'], selectedCluster.clusterStatus); + } + if (oldStatus === 'DELETE_IN_PROGRESS') { + clearState(['app', 'clusters', 'selected']); + navigate('/clusters'); + } + } +} + function ClusterList({ clusters }: ClusterListProps) { const selectedClusterName = useState(['app', 'clusters', 'selected']); let navigate = useNavigate(); let params = useParams(); const { t } = useTranslation(); - React.useEffect(() => { - const timerId = (setInterval(() => updateClusterList(navigate), 5000)); - return () => { clearInterval(timerId); } - }, []) - React.useEffect(() => { if(params.clusterName && selectedClusterName !== params.clusterName) selectCluster(params.clusterName, navigate); @@ -165,10 +152,19 @@ function ClusterList({ clusters }: ClusterListProps) { export default function Clusters () { const clusterName = useState(['app', 'clusters', 'selected']); - const cluster = useState(['clusters', 'index', clusterName]); const [ splitOpen, setSplitOpen ] = React.useState(true); const { t } = useTranslation(); - const { data } = useQuery('LIST_CLUSTERS', () => ListClusters()); + const { data } = useQuery('LIST_CLUSTERS', () => ListClusters(), { + refetchInterval: 5000, + }); + const selectedClusterName = useState(['app', 'clusters', 'selected']); + const oldStatus = useState(['app', 'clusters', 'selectedStatus']); + let navigate = useNavigate(); + + useEffect( + () => onClustersUpdate(selectedClusterName, data, oldStatus, navigate), + [selectedClusterName, oldStatus, data, navigate], + ); return ( ({ jest.mock('../../../store', () => ({ ...jest.requireActual('../../../store') as any, isAdmin: () => true, + setState: jest.fn(), + clearState: jest.fn(), })); -const mockedUseNavigate = jest.fn(); +const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom') as any, - useNavigate: () => mockedUseNavigate, + useNavigate: () => mockNavigate, })); describe('given a component to show the clusters list', () => { @@ -57,7 +59,7 @@ describe('given a component to show the clusters list', () => { describe('when the clusters list is available', () => { beforeEach(() => { (ListClusters as jest.Mock).mockResolvedValue(mockClusters); - mockedUseNavigate.mockReset(); + mockNavigate.mockReset(); }); it('should render the clusters', async () => { @@ -81,7 +83,7 @@ describe('given a component to show the clusters list', () => { )) await userEvent.click(output.getByRole('radio')) - expect(mockedUseNavigate).toHaveBeenCalledWith('/clusters/test-cluster') + expect(mockNavigate).toHaveBeenCalledWith('/clusters/test-cluster') }) }) @@ -94,7 +96,7 @@ describe('given a component to show the clusters list', () => { )) await userEvent.click(output.getByText('Create Cluster')) - expect(mockedUseNavigate).toHaveBeenCalledWith('/configure') + expect(mockNavigate).toHaveBeenCalledWith('/configure') }) }) }) @@ -115,3 +117,36 @@ describe('given a component to show the clusters list', () => { }) }) }) + +describe("Given a list of clusters", () => { + beforeEach(() => jest.resetAllMocks()); + describe("when a cluster is selected and the list is updated", () => { + describe("when the cluster has a new status", () => { + it("should be saved", () => { + onClustersUpdate("test-cluster", mockClusters, "CREATE_IN_PROGRESS", mockNavigate); + + expect(setState).toHaveBeenCalledWith(['app', 'clusters', 'selectedStatus'], "CREATE_COMPLETE"); + }); + }); + + describe("when the cluster has the same status", () => { + it("should not be updated", () => { + onClustersUpdate("test-cluster", mockClusters, "CREATE_COMPLETE", mockNavigate); + + expect(setState).not.toHaveBeenCalled(); + }); + }); + + describe("when a cluster is deleted", () => { + beforeEach(() => { + onClustersUpdate("test-cluster", mockClusters, "DELETE_IN_PROGRESS", mockNavigate); + }); + it("should become unselected", () => { + expect(clearState).toHaveBeenCalledWith(['app', 'clusters', 'selected']); + }); + it("should navigate to the clusters list", () => { + expect(mockNavigate).toHaveBeenCalledWith('/clusters'); + }); + }); + }) +});