diff --git a/frontend/locales/en/strings.json b/frontend/locales/en/strings.json index e66fae35..d63e3085 100644 --- a/frontend/locales/en/strings.json +++ b/frontend/locales/en/strings.json @@ -4,6 +4,44 @@ "home": "Home" } }, + "cluster": { + "list": { + "filtering": { + "empty": { + "title": "No clusters", + "subtitle": "No clusters to display", + "action": "Create Cluster" + }, + "noMatch": { + "title": "No matches", + "subtitle": "No clusters match the filters", + "action": "Create filter" + } + }, + "cols": { + "name": "Name", + "status": "Status", + "version": "Version" + }, + "loadingText": "Loading clusters...", + "countText": "Results:", + "filteringAriaLabel": "Filter cluster", + "splitPanel": { + "preferencesTitle": "Split panel preferences", + "preferencesPositionLabel": "Split panel position", + "preferencesPositionDescription": "Choose the default split panel position for the service.", + "preferencesPositionSide": "Side", + "preferencesPositionBottom": "Bottom", + "preferencesConfirm": "Confirm", + "preferencesCancel": "Cancel", + "closeButtonAriaLabel": "Close panel", + "openButtonAriaLabel": "Open panel", + "resizeHandleAriaLabel": "Resize split panel", + "noClusterSelectedText": "No cluster selected", + "selectClusterText": "Select a cluster to see its details" + } + } + }, "wizard": { "source" : { "validation": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 77a79dc1..3ead8f46 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,6 +37,7 @@ "devDependencies": { "@awsui/jest-preset": "^2.0.5", "@testing-library/react": "^12.1.5", + "@testing-library/user-event": "^14.3.0", "@types/jest": "^28.1.4", "@types/lodash": "^4.14.182", "@types/node": "^18.0.0", @@ -3952,6 +3953,19 @@ "react-dom": "<18.0.0" } }, + "node_modules/@testing-library/user-event": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.3.0.tgz", + "integrity": "sha512-P02xtBBa8yMaLhK8CzJCIns8rqwnF6FxhR9zs810flHOBXUYCFjLd8Io1rQrAkQRWEmW2PGdZIEdMxf/KLsqFA==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -17344,6 +17358,13 @@ "@types/react-dom": "<18.0.0" } }, + "@testing-library/user-event": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.3.0.tgz", + "integrity": "sha512-P02xtBBa8yMaLhK8CzJCIns8rqwnF6FxhR9zs810flHOBXUYCFjLd8Io1rQrAkQRWEmW2PGdZIEdMxf/KLsqFA==", + "dev": true, + "requires": {} + }, "@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index fdabd35b..03cec09c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,6 +55,7 @@ "devDependencies": { "@awsui/jest-preset": "^2.0.5", "@testing-library/react": "^12.1.5", + "@testing-library/user-event": "^14.3.0", "@types/jest": "^28.1.4", "@types/lodash": "^4.14.182", "@types/node": "^18.0.0", diff --git a/frontend/src/model.tsx b/frontend/src/model.tsx index 3c652f12..3f97b04e 100644 --- a/frontend/src/model.tsx +++ b/frontend/src/model.tsx @@ -163,19 +163,18 @@ function DeleteCluster(clusterName: any, callback=null) { }) } -function ListClusters(callback: any) { +async function ListClusters() { var url = 'api?path=/v3/clusters'; - request('get', url).then((response: any) => { - //console.log("List Success", response) - if(response.status === 200) { - callback && callback(response.data.clusters); - setState(['clusters', 'list'], response.data.clusters); + try { + const { data } = await request('get', url); + setState(['clusters', 'list'], data?.clusters); + return data?.clusters || []; + } catch (error) { + if((error as any).response) { + notify(`Error: ${(error as any).response.data.message}`, 'error'); } - }).catch((error: any) => { - if(error.response) - notify(`Error: ${error.response.data.message}`, 'error'); - console.log(error) - }); + throw error; + } } function GetConfiguration(clusterName: any, callback=null) { @@ -803,7 +802,6 @@ async function LoadInitialState() { if(groups && (groups.includes("admin") || groups.includes("user"))) { ListUsers(); - // @ts-expect-error TS(2554) FIXME: Expected 1 arguments, but got 0. ListClusters(); // @ts-expect-error TS(2554) FIXME: Expected 3 arguments, but got 0. ListCustomImages(); diff --git a/frontend/src/old-pages/Clusters/Clusters.tsx b/frontend/src/old-pages/Clusters/Clusters.tsx index d6d8d016..75991173 100644 --- a/frontend/src/old-pages/Clusters/Clusters.tsx +++ b/frontend/src/old-pages/Clusters/Clusters.tsx @@ -9,14 +9,12 @@ // limitations under the License. import React from 'react'; import { NavigateFunction, useNavigate, useParams } from "react-router-dom" - import { ListClusters } from '../../model' - -import { useState, clearState, getState, setState, isAdmin } from '../../store' +import { useState, getState, clearState, setState, isAdmin } from '../../store' import { selectCluster } from './util' import { findFirst } from '../../util' - -// UI Elements +import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; import { AppLayout, Button, @@ -27,47 +25,56 @@ import { Table, TextFilter } from "@awsui/components-react"; - import { useCollection } from '@awsui/collection-hooks'; -// Components import EmptyState from '../../components/EmptyState'; import Status from "../../components/Status"; import Actions from './Actions'; import Details from "./Details"; import { wizardShow } from '../Configure/Configure'; -function updateClusterList(navigate: NavigateFunction) { + +export interface Cluster { + cloudformationStackArn: string, + cloudformationStackStatus: string, + clusterName: string, + clusterStatus: string, + region: string, + version: string +} + +async function updateClusterList(navigate: NavigateFunction) { const selectedClusterName = getState(['app', 'clusters', 'selected']); const oldStatus = getState(['app', 'clusters', 'selectedStatus']); - ListClusters((clusterList: any) => { - if(selectedClusterName) - { - const selectedCluster = findFirst(clusterList, (c: any) => c.clusterName === selectedClusterName); - if(selectedCluster) - { - if(oldStatus !== selectedCluster.clusterStatus) + 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')) - // @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1. - selectCluster(selectedClusterName); - // If the selected cluster is not found, and was in DELETE_IN_PROGRESS status, deselect it - } else if (oldStatus === 'DELETE_IN_PROGRESS') { + } + 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) {} } -function ClusterList() { - let clusters = useState(['clusters', 'list']); +type ClusterListProps = { + clusters: Cluster[] +} + +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)); @@ -77,7 +84,12 @@ function ClusterList() { React.useEffect(() => { if(params.clusterName && selectedClusterName !== params.clusterName) selectCluster(params.clusterName, navigate); - }, [selectedClusterName, params]) + }, [selectedClusterName, params, navigate]) + + const onSelectionChangeCallback = React.useCallback( + ({ detail }) => { + navigate(`/clusters/${(detail.selectedItems[0] as Cluster).clusterName}`); + }, [navigate]); const configure = () => { wizardShow(navigate); @@ -89,16 +101,16 @@ function ClusterList() { filtering: { empty: ( Create Cluster} + title={t("cluster.list.filtering.empty.title")} + subtitle={t("cluster.list.filtering.empty.subtitle")} + action={} /> ), noMatch: ( actions.setFiltering('')}>Clear filter} + title={t("cluster.list.filtering.noMatch.title")} + subtitle={t("cluster.list.filtering.noMatch.subtitle")} + action={} /> ), }, @@ -115,19 +127,19 @@ function ClusterList() { } trackBy="clusterName" columnDefinitions={[ { id: "name", - header: "Name", + header: t("cluster.list.cols.name"), cell: item => (item as any).clusterName, sortingField: "clusterName" }, { id: "status", - header: "Status", + header: t("cluster.list.cols.status"), cell: item => || "-", sortingField: "clusterStatus" }, { id: "version", - header: "Version", + header: t("cluster.list.cols.version"), cell: item => (item as any).version || "-" } @@ -135,14 +147,13 @@ function ClusterList() { loading={clusters === null} items={items} selectionType="single" - loadingText="Loading clusters..." + loadingText={t("cluster.list.loadingText")} pagination={} filter={} + countText={`${t("cluster.list.countText")} ${filteredItemsCount}`} + filteringAriaLabel={t("cluster.list.filteringAriaLabel")}/>} selectedItems={(items || []).filter((c) => (c as any).clusterName === selectedClusterName)} - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - onSelectionChange={({ detail }) => { navigate(`/clusters/${detail.selectedItems[0].clusterName}`); }} + onSelectionChange={onSelectionChangeCallback} /> ) } @@ -150,18 +161,9 @@ function ClusterList() { export default function Clusters () { const clusterName = useState(['app', 'clusters', 'selected']); const cluster = useState(['clusters', 'index', clusterName]); - const clusters = useState(['clusters', 'list']); - let navigate = useNavigate(); const [ splitOpen, setSplitOpen ] = React.useState(true); - - const configure = () => { - wizardShow(navigate); - } - - React.useEffect(() => { - // @ts-expect-error TS(2554) FIXME: Expected 1 arguments, but got 0. - ListClusters(); - }, []) + const { t } = useTranslation(); + const { data } = useQuery('LIST_CLUSTERS', () => ListClusters()); return ( }> - {clusterName ? `Cluster: ${clusterName}` : "No cluster selected" } + actions={cluster && }> + {clusterName ? `Cluster: ${clusterName}` : t("cluster.list.splitPanel.noClusterSelectedText") } }> - {clusterName ?
:
Select a cluster to see its details.
} + {clusterName ?
:
{t("cluster.list.splitPanel.selectClusterText")}
} } - content={ + content={ } /> ); diff --git a/frontend/src/old-pages/Clusters/__tests__/Clusters.test.tsx b/frontend/src/old-pages/Clusters/__tests__/Clusters.test.tsx new file mode 100644 index 00000000..97c30e3f --- /dev/null +++ b/frontend/src/old-pages/Clusters/__tests__/Clusters.test.tsx @@ -0,0 +1,120 @@ +import { ThemeProvider } from '@emotion/react' +import { createTheme } from '@mui/material' +import { render, waitFor, screen, prettyDOM } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { SnackbarProvider } from 'notistack' +import { I18nextProvider } from 'react-i18next' +import { QueryClient, QueryClientProvider } from 'react-query' +import { Provider } from 'react-redux' +import { BrowserRouter, useNavigate } from 'react-router-dom' +import i18n from '../../../i18n' +import { ListClusters } from '../../../model' +import { store, isAdmin } from '../../../store' +import Clusters from '../Clusters' + + +const queryClient = new QueryClient(); +const mockClusters = [{ + clusterName: 'test-cluster', + clusterStatus: 'CREATE_COMPLETE', + version: '3.1.4', + cloudformationStackArn: 'arn', + region: 'region', + cloudformationStackStatus: 'status' +}]; + +const MockProviders = (props: any) => ( + + + + + + + {props.children} + + + + + + +) + +jest.mock('../../../model', () => ({ + ListClusters: jest.fn(), +})); + +jest.mock('../../../store', () => ({ + ...jest.requireActual('../../../store') as any, + isAdmin: () => true, +})); + +const mockedUseNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom') as any, + useNavigate: () => mockedUseNavigate, +})); + +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(); + }); + + it('should render the clusters', async () => { + const { getByText } = await waitFor(() => render( + + + + )) + + expect(getByText('test-cluster')).toBeTruthy() + expect(getByText('CREATE COMPLETE')).toBeTruthy() + expect(getByText('3.1.4')).toBeTruthy() + }) + + describe('when the user selects a cluster', () => { + it('should populate the split panel', async () => { + const output = await waitFor(() => render( + + + + )) + + await userEvent.click(output.getByRole('radio')) + expect(mockedUseNavigate).toHaveBeenCalledWith('/clusters/test-cluster') + }) + }) + + describe('when the user clicks on "Create Cluster" button', () => { + it('should redirect to configure', async () => { + const output = await waitFor(() => render( + + + + )) + + await userEvent.click(output.getByText('Create Cluster')) + expect(mockedUseNavigate).toHaveBeenCalledWith('/configure') + }) + }) + }) + + describe('when there are no clusters available', () => { + beforeEach(() => { + (ListClusters as jest.Mock).mockResolvedValue([]); + }); + + it('should show the empty state', async () => { + const { getByText } = await waitFor(() => render( + + + + )) + + expect(getByText('No clusters to display')).toBeTruthy() + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/old-pages/Configure/Create.tsx b/frontend/src/old-pages/Configure/Create.tsx index 60254ba3..d0050992 100644 --- a/frontend/src/old-pages/Configure/Create.tsx +++ b/frontend/src/old-pages/Configure/Create.tsx @@ -57,7 +57,6 @@ function handleCreate(handleClose: any, navigate: any) { // @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1. DescribeCluster(clusterName) setState(['app', 'clusters', 'selected'], clusterName); - // @ts-expect-error TS(2554) FIXME: Expected 1 arguments, but got 0. ListClusters(); handleClose(); navigate(href);