diff --git a/assets/.eslintrc.js b/assets/.eslintrc.js index 95080d8bd8..d98c1fc48b 100644 --- a/assets/.eslintrc.js +++ b/assets/.eslintrc.js @@ -6,6 +6,9 @@ module.exports = { jquery: true, 'jest/globals': true, }, + globals: { + process: true, + }, extends: ['eslint:recommended', 'plugin:react/recommended'], parserOptions: { ecmaFeatures: { diff --git a/assets/js/components/ChecksCatalog/CatalogContainer.jsx b/assets/js/components/ChecksCatalog/CatalogContainer.jsx new file mode 100644 index 0000000000..9c7f72fc5b --- /dev/null +++ b/assets/js/components/ChecksCatalog/CatalogContainer.jsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import NotificationBox from '@components/NotificationBox'; +import LoadingBox from '@components/LoadingBox'; + +import { groupBy } from '@lib/lists'; + +import { EOS_ERROR } from 'eos-icons-react'; + +import CheckItem from './CheckItem'; + +const CatalogContainer = ({ + onRefresh = () => {}, + catalogData = [], + catalogError = null, + loading = false, +}) => { + if (loading) { + return ; + } + + if (catalogError) { + return ( + } + text={catalogError} + buttonText="Try again" + buttonOnClick={onRefresh} + /> + ); + } + + if (catalogData.length === 0) { + return ( + } + text="Checks catalog is empty." + buttonText="Try again" + buttonOnClick={onRefresh} + /> + ); + } + + return ( +
+ {Object.entries(groupBy(catalogData, 'group')).map( + ([group, checks], idx) => ( +
+
+

+ {group} +

+
+
    + {checks.map((check) => ( + + ))} +
+
+ ) + )} +
+ ); +}; + +export default CatalogContainer; diff --git a/assets/js/components/ChecksCatalog/CatalogContainer.test.jsx b/assets/js/components/ChecksCatalog/CatalogContainer.test.jsx new file mode 100644 index 0000000000..39428f280b --- /dev/null +++ b/assets/js/components/ChecksCatalog/CatalogContainer.test.jsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { screen, within } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { faker } from '@faker-js/faker'; +import { renderWithRouter } from '@lib/test-utils'; +import { catalogCheckFactory } from '@lib/test-utils/factories'; + +import CatalogContainer from './CatalogContainer'; + +describe('ChecksCatalog CatalogContainer component', () => { + it('should render the notification box', () => { + renderWithRouter(); + + expect(screen.getByText('some error')).toBeVisible(); + expect(screen.getByRole('button')).toHaveTextContent('Try again'); + }); + + it('should render the loading box', () => { + renderWithRouter(); + + expect(screen.getByText('Loading checks catalog...')).toBeVisible(); + }); + + it('should render an error message if the checks catalog is empty', () => { + renderWithRouter(); + + expect(screen.getByText('Checks catalog is empty.')).toBeVisible(); + expect(screen.getByRole('button')).toHaveTextContent('Try again'); + }); + + it('should render the checks catalog', () => { + const groupName1 = faker.animal.cat(); + const groupName2 = faker.animal.cat(); + const group1 = catalogCheckFactory.buildList(5, { group: groupName1 }); + const group2 = catalogCheckFactory.buildList(5, { group: groupName2 }); + const catalog = group1.concat(group2); + + renderWithRouter( + + ); + + const groups = screen.getAllByRole('list'); + expect(groups.length).toBe(2); + + for (let group of groups) { + let { getAllByRole } = within(group); + let checks = getAllByRole('listitem'); + expect(checks.length).toBe(5); + } + }); +}); diff --git a/assets/js/components/ChecksCatalog/CheckItem.jsx b/assets/js/components/ChecksCatalog/CheckItem.jsx new file mode 100644 index 0000000000..0f274815c6 --- /dev/null +++ b/assets/js/components/ChecksCatalog/CheckItem.jsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import { Disclosure, Transition } from '@headlessui/react'; + +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +const CheckItem = ({ checkID, premium = false, description, remediation }) => { + return ( +
  • + + +
    +
    +

    + {checkID} +

    + {premium > 0 && ( +

    + Premium +

    + )} +
    +
    +
    + + {description} + +
    +
    +
    +
    + + +
    +
    + + {remediation} + +
    +
    +
    +
    +
    +
  • + ); +}; + +export default CheckItem; diff --git a/assets/js/components/ChecksCatalog/CheckItem.test.jsx b/assets/js/components/ChecksCatalog/CheckItem.test.jsx new file mode 100644 index 0000000000..f13440178b --- /dev/null +++ b/assets/js/components/ChecksCatalog/CheckItem.test.jsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import { screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; + +import { renderWithRouter } from '@lib/test-utils'; +import { catalogCheckFactory } from '@lib/test-utils/factories'; + +import CheckItem from './CheckItem'; + +describe('ChecksCatalog CheckItem component', () => { + it('should show check information', () => { + const check = catalogCheckFactory.build(); + + renderWithRouter( + + ); + + expect(screen.getByText(check.id)).toBeVisible(); + expect(screen.getByText(check.description)).toBeVisible(); + }); + + it('should show premium badge if the check is premium', () => { + const check = catalogCheckFactory.build(); + + renderWithRouter( + + ); + + expect(screen.getByText('Premium')).toBeVisible(); + }); + + it('should show check remediation when the row is clicked', () => { + const check = catalogCheckFactory.build(); + + renderWithRouter( + + ); + + const checks = screen.getAllByRole('listitem'); + const checkDiv = checks[0].querySelector('div'); + + expect(screen.queryByText(check.remediation)).not.toBeInTheDocument(); + userEvent.click(checkDiv); + expect(screen.getByText(check.remediation)).toBeVisible(); + userEvent.click(checkDiv); + expect(screen.queryByText(check.remediation)).not.toBeInTheDocument(); + }); +}); diff --git a/assets/js/components/ChecksCatalog/ChecksCatalogNew.jsx b/assets/js/components/ChecksCatalog/ChecksCatalogNew.jsx new file mode 100644 index 0000000000..bd2ec58885 --- /dev/null +++ b/assets/js/components/ChecksCatalog/ChecksCatalogNew.jsx @@ -0,0 +1,41 @@ +import React, { useState, useEffect } from 'react'; + +import axios from 'axios'; + +import CatalogContainer from './CatalogContainer'; + +const wandaURL = process.env.WANDA_URL; + +export const ChecksCatalogNew = () => { + const [catalogError, setError] = useState(null); + const [loading, setLoaded] = useState(true); + const [catalogData, setCatalog] = useState([]); + + useEffect(() => { + getCatalog(); + }, []); + + const getCatalog = () => { + setLoaded(true); + axios + .get(`${wandaURL}/api/checks/catalog`) + .then((catalog) => { + setCatalog(catalog.data.items); + }) + .catch((error) => { + setError(error.message); + }) + .finally(() => { + setLoaded(false); + }); + }; + + return ( + getCatalog()} + catalogData={catalogData} + catalogError={catalogError} + loading={loading} + /> + ); +}; diff --git a/assets/js/components/ChecksCatalog/index.js b/assets/js/components/ChecksCatalog/index.js index e2133b19cf..28776e7e6b 100644 --- a/assets/js/components/ChecksCatalog/index.js +++ b/assets/js/components/ChecksCatalog/index.js @@ -1,3 +1,5 @@ import ChecksCatalog from './ChecksCatalog'; +export { ChecksCatalogNew } from './ChecksCatalogNew'; + export default ChecksCatalog; diff --git a/assets/js/lib/test-utils/factories.js b/assets/js/lib/test-utils/factories.js index b9841c65a9..2bf400327c 100644 --- a/assets/js/lib/test-utils/factories.js +++ b/assets/js/lib/test-utils/factories.js @@ -27,3 +27,11 @@ export const healthSummaryFactory = Factory.define(() => ({ casing: 'upper', }), })); + +export const catalogCheckFactory = Factory.define(() => ({ + id: faker.datatype.uuid(), + name: faker.animal.cat(), + group: faker.animal.cat(), + description: faker.lorem.paragraph(), + remediation: faker.lorem.paragraph(), +})); diff --git a/assets/js/trento.jsx b/assets/js/trento.jsx index 85904fc9d6..96a1588fff 100644 --- a/assets/js/trento.jsx +++ b/assets/js/trento.jsx @@ -23,7 +23,7 @@ import ClusterDetails from '@components/ClusterDetails'; import DatabasesOverview from '@components/DatabasesOverview'; import SapSystemDetails from './components/SapSystemDetails/SapSystemDetails'; import DatabaseDetails from './components/DatabaseDetails'; -import ChecksCatalog from '@components/ChecksCatalog'; +import ChecksCatalog, { ChecksCatalogNew } from '@components/ChecksCatalog'; import NotFound from '@components/NotFound'; import SomethingWentWrong from '@components/SomethingWentWrong'; import Settings from '@components/Settings'; @@ -49,6 +49,7 @@ const App = () => { } /> } /> } /> + } /> } /> } />