Skip to content

Commit

Permalink
Catalog from wanda (#960)
Browse files Browse the repository at this point in the history
* Duplicate current catalog view in a new route

* Implement new catalog container

* Add tests to the catalog container

* Include process as global variable

* Show error message if the checks catalog is empty

* Extract check item to individual component
  • Loading branch information
arbulu89 authored Nov 8, 2022
1 parent 696aa67 commit ff8d820
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 1 deletion.
3 changes: 3 additions & 0 deletions assets/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module.exports = {
jquery: true,
'jest/globals': true,
},
globals: {
process: true,
},
extends: ['eslint:recommended', 'plugin:react/recommended'],
parserOptions: {
ecmaFeatures: {
Expand Down
75 changes: 75 additions & 0 deletions assets/js/components/ChecksCatalog/CatalogContainer.jsx
Original file line number Diff line number Diff line change
@@ -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 <LoadingBox text="Loading checks catalog..." />;
}

if (catalogError) {
return (
<NotificationBox
icon={<EOS_ERROR className="m-auto" color="red" size="xl" />}
text={catalogError}
buttonText="Try again"
buttonOnClick={onRefresh}
/>
);
}

if (catalogData.length === 0) {
return (
<NotificationBox
icon={<EOS_ERROR className="m-auto" color="red" size="xl" />}
text="Checks catalog is empty."
buttonText="Try again"
buttonOnClick={onRefresh}
/>
);
}

return (
<div>
{Object.entries(groupBy(catalogData, 'group')).map(
([group, checks], idx) => (
<div
key={idx}
className="check-group bg-white shadow overflow-hidden sm:rounded-md mb-8"
>
<div className="bg-white px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
{group}
</h3>
</div>
<ul role="list" className="divide-y divide-gray-200">
{checks.map((check) => (
<CheckItem
key={check.id}
checkID={check.id}
premium={check.premium}
description={check.description}
remediation={check.remediation}
/>
))}
</ul>
</div>
)
)}
</div>
);
};

export default CatalogContainer;
57 changes: 57 additions & 0 deletions assets/js/components/ChecksCatalog/CatalogContainer.test.jsx
Original file line number Diff line number Diff line change
@@ -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(<CatalogContainer catalogError={'some error'} />);

expect(screen.getByText('some error')).toBeVisible();
expect(screen.getByRole('button')).toHaveTextContent('Try again');
});

it('should render the loading box', () => {
renderWithRouter(<CatalogContainer loading={true} />);

expect(screen.getByText('Loading checks catalog...')).toBeVisible();
});

it('should render an error message if the checks catalog is empty', () => {
renderWithRouter(<CatalogContainer catalogData={[]} />);

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(
<CatalogContainer
loading={false}
catalogError={null}
catalogData={catalog}
/>
);

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);
}
});
});
62 changes: 62 additions & 0 deletions assets/js/components/ChecksCatalog/CheckItem.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<li>
<Disclosure>
<Disclosure.Button
as="div"
className="flex justify-between w-full cursor-pointer hover:bg-gray-100"
>
<div className="check-row px-4 py-4 sm:px-6">
<div className="flex items-center">
<p className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
{checkID}
</p>
{premium > 0 && (
<p className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Premium
</p>
)}
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="sm:flex">
<ReactMarkdown
className="markdown text-sm"
remarkPlugins={[remarkGfm]}
>
{description}
</ReactMarkdown>
</div>
</div>
</div>
</Disclosure.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-100 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel className="check-panel border-none">
<div className="px-8 py-4 sm:px-8">
<div className="px-4 py-4 sm:px-4 bg-slate-100 rounded">
<ReactMarkdown className="markdown" remarkPlugins={[remarkGfm]}>
{remediation}
</ReactMarkdown>
</div>
</div>
</Disclosure.Panel>
</Transition>
</Disclosure>
</li>
);
};

export default CheckItem;
66 changes: 66 additions & 0 deletions assets/js/components/ChecksCatalog/CheckItem.test.jsx
Original file line number Diff line number Diff line change
@@ -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(
<CheckItem
key={check.id}
checkID={check.id}
description={check.description}
remediation={check.remediation}
/>
);

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(
<CheckItem
key={check.id}
checkID={check.id}
premium={true}
description={check.description}
remediation={check.remediation}
/>
);

expect(screen.getByText('Premium')).toBeVisible();
});

it('should show check remediation when the row is clicked', () => {
const check = catalogCheckFactory.build();

renderWithRouter(
<CheckItem
key={check.id}
checkID={check.id}
description={check.description}
remediation={check.remediation}
/>
);

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();
});
});
41 changes: 41 additions & 0 deletions assets/js/components/ChecksCatalog/ChecksCatalogNew.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<CatalogContainer
onRefresh={() => getCatalog()}
catalogData={catalogData}
catalogError={catalogError}
loading={loading}
/>
);
};
2 changes: 2 additions & 0 deletions assets/js/components/ChecksCatalog/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ChecksCatalog from './ChecksCatalog';

export { ChecksCatalogNew } from './ChecksCatalogNew';

export default ChecksCatalog;
8 changes: 8 additions & 0 deletions assets/js/lib/test-utils/factories.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}));
3 changes: 2 additions & 1 deletion assets/js/trento.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -49,6 +49,7 @@ const App = () => {
<Route path="sap_systems" element={<SapSystemsOverview />} />
<Route path="databases" element={<DatabasesOverview />} />
<Route path="catalog" element={<ChecksCatalog />} />
<Route path="catalog_new" element={<ChecksCatalogNew />} />
<Route path="settings" element={<Settings />} />
<Route path="about" element={<AboutPage />} />
<Route
Expand Down

0 comments on commit ff8d820

Please sign in to comment.