Skip to content

Commit

Permalink
Fixes #29298 - Add new Content View table
Browse files Browse the repository at this point in the history
This adds a new content view table using patternfly 4 to the page "/labs/content_views"
  • Loading branch information
John Mitsch committed May 6, 2020
1 parent e39a522 commit 44f7120
Show file tree
Hide file tree
Showing 40 changed files with 1,091 additions and 31 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
webpack/foremanReact
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ node_modules
npm-debug.log
/foreman/
package-lock.json
webpack/foremanReact/
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module.exports = {
],
setupFilesAfterEnv: [
'./webpack/global_test_setup.js',
'@testing-library/jest-dom'
],
testPathIgnorePatterns: [
'/node_modules/',
Expand Down
4 changes: 2 additions & 2 deletions lib/katello/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,10 @@

menu :labs_menu,
:content_publication,
:url => '/labs/content_publication',
:url => '/labs/content_views',
:url_hash => {:controller => 'katello/react',
:action => 'index'},
:caption => N_('Content Publication'),
:caption => N_('Content Views'),
:parent => :lab_features_menu,
:turbolinks => false

Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
},
"devDependencies": {
"@babel/core": "^7.7.0",
"@sheerun/mutationobserver-shim": "^0.3.3",
"@storybook/react": "^3.2.17",
"@storybook/storybook-deployer": "^2.0.0",
"@testing-library/jest-dom": "^5.3.0",
"@testing-library/react": "^10.0.2",
"@theforeman/builder": "^4.2.0",
"@theforeman/vendor-dev": "^4.2.0",
"axios-mock-adapter": "^1.10.0",
Expand All @@ -47,13 +50,16 @@
"eslint-plugin-react": "^7.4.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.9.0",
"nock": "^12.0.3",
"prettier": "^1.7.4",
"react-redux-test-utils": "^0.1.1",
"react-test-renderer": "^16.0.0",
"redux-mock-store": "^1.3.0"
},
"_comment": "We don't include @theforeman/vendor because it's assumed to be present in Foreman",
"dependencies": {
"@patternfly/react-icons": "^3.15.15",
"@patternfly/react-tokens": "^2.8.13",
"angular": "1.7.9",
"bootstrap-select": "1.12.4",
"downshift": "^1.28.0",
Expand Down
8 changes: 7 additions & 1 deletion webpack/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
"specialLink": [ "to" ]
}],
"promise/prefer-await-to-then": "error",
"promise/prefer-await-to-callbacks": "error"
"promise/prefer-await-to-callbacks": "error",
"no-unused-vars": ["error", {
"vars": "all",
"args": "after-used",
"ignoreRestSiblings": true,
"argsIgnorePattern": "^_"
}]
}
}
6 changes: 0 additions & 6 deletions webpack/__mocks__/foremanReact/redux/API.js

This file was deleted.

6 changes: 3 additions & 3 deletions webpack/containers/Application/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ModuleStreams from '../../scenes/ModuleStreams';
import ModuleStreamDetails from '../../scenes/ModuleStreams/Details';
import AnsibleCollections from '../../scenes/AnsibleCollections';
import AnsibleCollectionDetails from '../../scenes/AnsibleCollections/Details';
import ContentPublication from '../../scenes/ContentPublication';
import ContentViews from '../../scenes/ContentViews';
import withHeader from './withHeaders';

// eslint-disable-next-line import/prefer-default-export
Expand Down Expand Up @@ -52,7 +52,7 @@ export const links = [
component: withHeader(AnsibleCollectionDetails, { title: __('Ansible Collection Details') }),
},
{
path: 'labs/content_publication',
component: withHeader(ContentPublication, { title: __('Content Publication') }),
path: 'labs/content_views',
component: withHeader(ContentViews, { title: __('Content Views') }),
},
];
8 changes: 8 additions & 0 deletions webpack/containers/Application/overrides.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ td, th {
word-wrap: break-word;
}

// needed because we have overflow set to auto for all td's and it affected the pf4 table
.katello-pf4-table {
td {
max-width: 200px;
overflow: unset;
word-wrap: break-word;
}
}
// override foreman's .editable styles, that are conflicting with the patternfly ones
.pf-table-inline-edit {
.editable {
Expand Down
17 changes: 17 additions & 0 deletions webpack/mockRequest.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import nock from 'nock';

// TODO: figure out way to reuse this from foreman
export const mock = new MockAdapter(axios);
Expand Down Expand Up @@ -30,3 +31,19 @@ export const mockErrorRequest = ({
});

export const mockReset = () => mock.reset();

// Using the library 'nock' as it matches actual network requests rather than mock another
// library. This is helpful when the request is not coming from Katello. For example, axios
// called within Katello can be mocked with axios-mock-adapter or similar, but a http request
// made by axios that is coming from Foreman cannot be mocked by axios-mock-adapter or a
// jest mock within Katello. So to do this, we can mock the request a level deeper within
// nodejs by using nock.
export const nockInstance = nock('http://localhost');

// Calling .done() with nock asserts that the request was fufilled. We use a timeout to ensure
// that the component has set up and made the request before the assertion is made.
export const assertNockRequest = (scope, timeout = 2000) => {
setTimeout(() => {
scope.done();
}, timeout);
};
3 changes: 0 additions & 3 deletions webpack/scenes/ContentPublication/index.js

This file was deleted.

13 changes: 13 additions & 0 deletions webpack/scenes/ContentViews/ContentViewSelectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {
selectAPIStatus,
selectAPIError,
selectAPIResponse,
} from 'foremanReact/redux/API/APISelectors';
import CONTENT_VIEWS_KEY from './ContentViewsConstants';

export const selectContentViews = state =>
selectAPIResponse(state, CONTENT_VIEWS_KEY).results || [];

export const selectContentViewStatus = state => selectAPIStatus(state, CONTENT_VIEWS_KEY);

export const selectContentViewError = state => selectAPIError(state, CONTENT_VIEWS_KEY);
18 changes: 18 additions & 0 deletions webpack/scenes/ContentViews/ContentViewsActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { API_OPERATIONS, get } from 'foremanReact/redux/API';
import api, { orgId } from '../../services/api';
import CONTENT_VIEWS_KEY from './ContentViewsConstants';


const createContentViewsParams = () => ({
organization_id: orgId(),
nondefault: true,
});

const getContentViews = () => get({
type: API_OPERATIONS.GET,
key: CONTENT_VIEWS_KEY,
url: api.getApiUrl('/content_views'),
params: createContentViewsParams(),
});

export default getContentViews;
3 changes: 3 additions & 0 deletions webpack/scenes/ContentViews/ContentViewsConstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const CONTENT_VIEWS_KEY = 'contentViews';

export default CONTENT_VIEWS_KEY;
30 changes: 30 additions & 0 deletions webpack/scenes/ContentViews/ContentViewsPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useEffect } from 'react';
import { translate as __ } from 'foremanReact/common/I18n';
import { useSelector, useDispatch } from 'react-redux';
import getContentViews from './ContentViewsActions';
import { selectContentViews,
selectContentViewStatus,
selectContentViewError } from './ContentViewSelectors';

import ContentViewsTable from './Table/ContentViewsTable';

const ContentViewsPage = () => {
const items = useSelector(selectContentViews);
const status = useSelector(selectContentViewStatus);
const error = useSelector(selectContentViewError);

const dispatch = useDispatch();

useEffect(() => {
dispatch(getContentViews());
}, []);

return (
<React.Fragment>
<h1>{__('Content Views')}</h1>
<ContentViewsTable {...{ items, status, error }} />
</React.Fragment>
);
};

export default ContentViewsPage;
104 changes: 104 additions & 0 deletions webpack/scenes/ContentViews/Table/ContentViewsTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
import { STATUS } from 'foremanReact/constants';

import TableWrapper from './TableWrapper';
import tableDataGenerator from './tableDataGenerator';
import actionResolver from './actionResolver';

const ContentViewTable = ({
items, status, error,
}) => {
const [table, setTable] = useState({ rows: [], columns: [] });
const [rowMapping, setRowMapping] = useState({});
const loading = status === STATUS.PENDING;

useEffect(
() => {
if (!loading && items && items.length > 0) {
const { updatedRowMapping, ...tableData } = tableDataGenerator(
items,
rowMapping,
);
setTable(tableData);
setRowMapping(updatedRowMapping);
}
},
[items, JSON.stringify(rowMapping)], // use JSON to check obj values eq not reference eq
);

const cvIdFromRow = (rowIdx) => {
const entry = Object.entries(rowMapping).find(item => item[1].rowIndex === rowIdx);
if (entry) return parseInt(entry[0], 10);
return null;
};

const onSelect = (event, isSelected, rowId) => {
let rows;
if (rowId === -1) {
rows = table.rows.map(row => ({ ...row, selected: isSelected }));
} else {
rows = [...table.rows];
rows[rowId].selected = isSelected;
}

setTable(prevTable => ({ ...prevTable, rows }));
};

const onExpand = (_event, rowIndex, colIndex, isOpen) => {
const { rows } = table;
const contentViewId = cvIdFromRow(rowIndex);
// adjust for the selection checkbox cell being counted in the index
const adjustedColIndex = colIndex - 1;

if (!isOpen) {
setRowMapping((prev) => {
const updatedMap = { ...prev[contentViewId], expandedColumn: adjustedColIndex };
return { ...prev, [contentViewId]: updatedMap };
});
} else {
// remove the row completely by assigning it to a throwaway variable
// eslint-disable-next-line camelcase, no-unused-vars
const { [contentViewId]: _throwaway, ...newMap } = rowMapping;
setRowMapping(newMap);
}

setTable(prevTable => ({ ...prevTable, rows }));
};

const emptyTitle = __("You currently don't have any Content Views.");
const emptyBody = __('A Content View can be added by using the "New content view" button below.');

const { rows, columns } = table;
return (
<TableWrapper
rows={rows}
cells={columns}
status={status}
emptyTitle={emptyTitle}
emptyBody={emptyBody}
onSelect={onSelect}
canSelectAll={false}
onExpand={onExpand}
actionResolver={actionResolver}
error={error}
/>
);
};

ContentViewTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.shape({})),
status: PropTypes.string.isRequired,
error: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.string,
]),
};

ContentViewTable.defaultProps = {
error: null,
items: [],
};

export default ContentViewTable;
55 changes: 55 additions & 0 deletions webpack/scenes/ContentViews/Table/TableWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import {
Table,
TableHeader,
TableBody,
} from '@patternfly/react-table';
import PropTypes from 'prop-types';
import { STATUS } from 'foremanReact/constants';

import EmptyStateMessage from '../components/EmptyStateMessage';
import Loading from '../components/Loading';

const TableWrapper = ({
status, cells, rows, error, emptyTitle, emptyBody, ...extraTableProps
}) => {
if (status === STATUS.PENDING) return (<Loading />);
// Can we display the error message?
if (status === STATUS.ERROR) return (<EmptyStateMessage error={error} />);
// Can we prevent flash of empty row message while rows are loading with data?
if (status === STATUS.RESOLVED && rows.length === 0) {
return (<EmptyStateMessage title={emptyTitle} body={emptyBody} />);
}

const tableProps = { cells, rows, ...extraTableProps };
return (
<Table
aria-label="Content View Table"
className="katello-pf4-table"
{...tableProps}
>
<TableHeader />
<TableBody />
</Table>
);
};

TableWrapper.propTypes = {
status: PropTypes.string.isRequired,
cells: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.string])).isRequired,
rows: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
error: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.string,
]),
emptyBody: PropTypes.string.isRequired,
emptyTitle: PropTypes.string.isRequired,
};

TableWrapper.defaultProps = {
error: null,
};

export default TableWrapper;
Loading

0 comments on commit 44f7120

Please sign in to comment.