Skip to content

Commit

Permalink
Fixes #32930 - Add new OVAL content page (#486)
Browse files Browse the repository at this point in the history
  • Loading branch information
xprazak2 authored Oct 26, 2021
1 parent e5cc057 commit 6edf03d
Show file tree
Hide file tree
Showing 17 changed files with 517 additions and 15 deletions.
5 changes: 4 additions & 1 deletion webpack/components/EmptyState.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ const EmptyStateIcon = ({ error, search, lock }) => {
return <PfEmptyStateIcon icon={CubeIcon} />;
};

const EmptyState = ({ title, body, error, search, lock }) => (
const EmptyState = ({ title, body, error, search, lock, primaryButton }) => (
<Bullseye>
<PfEmptyState variant={EmptyStateVariant.small}>
<EmptyStateIcon error={!!error} search={search} lock={lock} />
<Title headingLevel="h2" size="lg">
{title}
</Title>
<EmptyStateBody>{body}</EmptyStateBody>
{primaryButton}
</PfEmptyState>
</Bullseye>
);
Expand All @@ -59,6 +60,7 @@ EmptyState.propTypes = {
error: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.string]),
search: PropTypes.bool,
lock: PropTypes.bool,
primaryButton: PropTypes.node,
};

EmptyState.defaultProps = {
Expand All @@ -68,6 +70,7 @@ EmptyState.defaultProps = {
error: undefined,
search: false,
lock: false,
primaryButton: null,
};

export default EmptyState;
15 changes: 11 additions & 4 deletions webpack/components/IndexLayout.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import ToastsList from 'foremanReact/components/ToastsList';
import {
Grid,
GridItem,
Expand All @@ -11,25 +12,31 @@ import {

import './IndexLayout.scss';

const IndexLayout = ({ pageTitle, children }) => (
const IndexLayout = ({ pageTitle, children, contentWidthSpan }) => (
<React.Fragment>
<Helmet>
<title>{pageTitle}</title>
</Helmet>
<ToastsList />
<Grid className="scap-page-grid">
<GridItem span={12}>
<GridItem span={12} className="pf-u-pb-xl">
<TextContent>
<Text component={TextVariants.h1}>{pageTitle}</Text>
</TextContent>
</GridItem>
<GridItem span={12}>{children}</GridItem>
<GridItem span={contentWidthSpan}>{children}</GridItem>
</Grid>
</React.Fragment>
);

IndexLayout.propTypes = {
pageTitle: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.object]).isRequired,
contentWidthSpan: PropTypes.number,
};

IndexLayout.defaultProps = {
contentWidthSpan: 12,
};

export default IndexLayout;
2 changes: 1 addition & 1 deletion webpack/components/IndexTable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const IndexTable = ({
IndexTable.propTypes = {
history: PropTypes.object.isRequired,
pagination: PropTypes.object.isRequired,
toolbarBtns: PropTypes.array,
toolbarBtns: PropTypes.node,
totalCount: PropTypes.number.isRequired,
ariaTableLabel: PropTypes.string.isRequired,
columns: PropTypes.array.isRequired,
Expand Down
26 changes: 26 additions & 0 deletions webpack/components/LinkButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@patternfly/react-core';
import PropTypes from 'prop-types';

const LinkButton = ({ path, btnVariant, btnText, isDisabled }) => (
<Link to={path}>
<Button variant={btnVariant} isDisabled={isDisabled}>
{btnText}
</Button>
</Link>
);

LinkButton.propTypes = {
path: PropTypes.string.isRequired,
btnText: PropTypes.string.isRequired,
btnVariant: PropTypes.string,
isDisabled: PropTypes.bool,
};

LinkButton.defaultProps = {
btnVariant: 'primary',
isDisabled: false,
};

export default LinkButton;
24 changes: 21 additions & 3 deletions webpack/components/withLoading.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
import Loading from 'foremanReact/components/Loading';
Expand Down Expand Up @@ -29,9 +29,17 @@ const withLoading = Component => {
renameData,
emptyStateTitle,
permissions,
primaryButton,
shouldRefetch,
...rest
}) => {
const { loading, error, data } = fetchFn(rest);
const { loading, error, data, refetch } = fetchFn(rest);

useEffect(() => {
if (shouldRefetch) {
refetch();
}
}, [shouldRefetch, refetch]);

if (loading) {
return <Loading />;
Expand Down Expand Up @@ -62,7 +70,13 @@ const withLoading = Component => {
const result = pluckData(data, resultPath);

if ((Array.isArray(result) && result.length === 0) || !result) {
return <EmptyState title={emptyStateTitle} body={emptyStateBody} />;
return (
<EmptyState
title={emptyStateTitle}
body={emptyStateBody}
primaryButton={primaryButton}
/>
);
}

return <Component {...rest} {...renameData(data)} />;
Expand All @@ -74,11 +88,15 @@ const withLoading = Component => {
renameData: PropTypes.func,
emptyStateTitle: PropTypes.string.isRequired,
permissions: PropTypes.array,
primaryButton: PropTypes.node,
shouldRefetch: PropTypes.bool,
};

Subcomponent.defaultProps = {
renameData: data => data,
permissions: [],
primaryButton: null,
shouldRefetch: false,
};

return Subcomponent;
Expand Down
63 changes: 63 additions & 0 deletions webpack/helpers/formFieldsHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';

import { FormGroup, TextInput } from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';

const wrapFieldProps = fieldProps => {
const { onChange } = fieldProps;
// modify onChange args to correctly wire formik with pf4 input handlers
const wrappedOnChange = (value, event) => {
onChange(event);
};

return { ...fieldProps, onChange: wrappedOnChange };
};

const shouldValidate = (form, fieldName) => {
if (form.touched[fieldName]) {
return form.errors[fieldName] ? 'error' : 'success';
}

return 'noval';
};

const fieldWithHandlers = Component => {
const Subcomponent = ({ label, form, field, isRequired, ...rest }) => {
const fieldProps = wrapFieldProps(field);
const valid = shouldValidate(form, field.name);

return (
<FormGroup
label={label}
isRequired={isRequired}
helperTextInvalid={form.errors[field.name]}
helperTextInvalidIcon={<ExclamationCircleIcon />}
validated={valid}
>
<Component
aria-label={fieldProps.name}
{...fieldProps}
{...rest}
validated={valid}
isDisabled={form.isSubmitting}
/>
</FormGroup>
);
};

Subcomponent.propTypes = {
form: PropTypes.object.isRequired,
field: PropTypes.object.isRequired,
label: PropTypes.string.isRequired,
isRequired: PropTypes.bool,
};

Subcomponent.defaultProps = {
isRequired: false,
};

return Subcomponent;
};

export const TextField = fieldWithHandlers(TextInput);
4 changes: 4 additions & 0 deletions webpack/helpers/pathsHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { decodeId } from './globalIdHelper';
const experimental = path => `/experimental${path}`;

const showPath = path => `${path}/:id`;
const newPath = path => `${path}/new`;

export const modelPath = (basePath, model) => `${basePath}/${decodeId(model)}`;

Expand All @@ -14,8 +15,11 @@ export const resolvePath = (path, params) =>
path
);

export const ovalContentsApiPath = '/api/v2/compliance/oval_contents';

export const ovalContentsPath = experimental('/compliance/oval_contents');
export const ovalContentsShowPath = showPath(ovalContentsPath);
export const ovalContentsNewPath = newPath(ovalContentsPath);
export const ovalPoliciesPath = experimental('/compliance/oval_policies');
export const ovalPoliciesShowPath = `${showPath(ovalPoliciesPath)}/:tab?`;
export const hostsPath = '/hosts';
Expand Down
10 changes: 10 additions & 0 deletions webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { useQuery } from '@apollo/client';
import { translate as __ } from 'foremanReact/common/I18n';

import IndexLayout from '../../../components/IndexLayout';
import LinkButton from '../../../components/LinkButton';
import OvalContentsTable from './OvalContentsTable';
import { ovalContentsNewPath } from '../../../helpers/pathsHelper';
import {
useParamsToVars,
useCurrentPagination,
Expand Down Expand Up @@ -48,6 +50,13 @@ const OvalContentsIndex = props => {
deleteOvalContentMutation,
__('OVAL Content')
)}
primaryButton={
<LinkButton
path={ovalContentsNewPath}
btnText={__('Create OVAL Content')}
/>
}
shouldRefetch={props.location?.state?.refreshOvalContents}
/>
</IndexLayout>
);
Expand All @@ -56,6 +65,7 @@ const OvalContentsIndex = props => {
OvalContentsIndex.propTypes = {
history: PropTypes.object.isRequired,
showToast: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
};

export default OvalContentsIndex;
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
import { Button } from '@patternfly/react-core';

import withLoading from '../../../components/withLoading';
import withDeleteModal from '../../../components/withDeleteModal';
import IndexTable from '../../../components/IndexTable';
import {
ovalContentsNewPath,
ovalContentsPath,
modelPath,
} from '../../../helpers/pathsHelper';

import { linkCell } from '../../../helpers/tableHelper';
import { ovalContentsPath, modelPath } from '../../../helpers/pathsHelper';

const OvalContentsTable = props => {
const columns = [
Expand Down Expand Up @@ -43,6 +48,16 @@ const OvalContentsTable = props => {
return actions;
};

const createBtn = (
<Button
onClick={() => props.history.push(ovalContentsNewPath)}
variant="primary"
aria-label="create_oval_content"
>
{__('Create OVAL Content')}
</Button>
);

return (
<IndexTable
columns={columns}
Expand All @@ -52,6 +67,7 @@ const OvalContentsTable = props => {
totalCount={props.totalCount}
history={props.history}
ariaTableLabel={__('OVAL Contents table')}
toolbarBtns={createBtn}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('OvalContentsIndex', () => {
render(
<TestComponent
history={historyMock}
location={{}}
mocks={mocks}
showToast={jest.fn()}
/>
Expand Down Expand Up @@ -55,6 +56,7 @@ describe('OvalContentsIndex', () => {
render(
<TestComponent
history={pageParamsHistoryMock}
location={{}}
mocks={mocked}
showToast={showToast}
/>
Expand All @@ -80,6 +82,7 @@ describe('OvalContentsIndex', () => {
render(
<TestComponent
history={pageParamsHistoryMock}
location={{}}
mocks={deleteMockFactory(firstCall, secondCall, [
{ message: 'is used by first policy', path: ['base'] },
{ message: 'is used by second policy', path: ['base'] },
Expand Down Expand Up @@ -107,6 +110,7 @@ describe('OvalContentsIndex', () => {
render(
<TestComponent
history={historyMock}
location={{}}
mocks={noDeleteMocks}
showToast={jest.fn()}
/>
Expand Down
Loading

0 comments on commit 6edf03d

Please sign in to comment.