Skip to content

Commit

Permalink
Fixes #36720 - Composable Expandable Table for Smart Proxy content
Browse files Browse the repository at this point in the history
(cherry picked from commit c5dc9dd)
  • Loading branch information
sjha4 committed Sep 6, 2023
1 parent bb738c6 commit 434a00b
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 13 deletions.
2 changes: 0 additions & 2 deletions app/views/foreman/smart_proxies/_content_tab.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
<%= javascript_include_tag *webpack_asset_paths('katello', extension: 'js') %>

<br />
<% @smartProxyId= @smart_proxy.id %>
<%= react_component('Content', smartProxyId: @smartProxyId,) %>
5 changes: 4 additions & 1 deletion webpack/components/Table/TableWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const TableWrapper = ({
emptySearchBody,
hideSearch,
alwaysHideToolbar,
hidePagination,
nodesBelowSearch,
bookmarkController,
readOnlyBookmarks,
Expand All @@ -59,7 +60,7 @@ const TableWrapper = ({
const { pageRowCount } = getPageStats({ total, page, perPage });
const unresolvedStatus = !!allTableProps?.status && allTableProps.status !== STATUS.RESOLVED;
const unresolvedStatusOrNoRows = unresolvedStatus || pageRowCount === 0;
const showPagination = !unresolvedStatusOrNoRows;
const showPagination = !unresolvedStatusOrNoRows && !hidePagination;
const filtersAreActive = activeFilters?.length &&
!isEqual(new Set(activeFilters), new Set(allTableProps.defaultFilters));
const hideToolbar = alwaysHideToolbar || (!searchQuery && !filtersAreActive &&
Expand Down Expand Up @@ -308,6 +309,7 @@ TableWrapper.propTypes = {
emptySearchBody: PropTypes.string,
hideSearch: PropTypes.bool,
alwaysHideToolbar: PropTypes.bool,
hidePagination: PropTypes.bool,
nodesBelowSearch: PropTypes.node,
bookmarkController: PropTypes.string,
readOnlyBookmarks: PropTypes.bool,
Expand Down Expand Up @@ -338,6 +340,7 @@ TableWrapper.defaultProps = {
emptySearchBody: __('Try changing your search settings.'),
hideSearch: false,
alwaysHideToolbar: false,
hidePagination: false,
nodesBelowSearch: null,
bookmarkController: undefined,
readOnlyBookmarks: false,
Expand Down
4 changes: 2 additions & 2 deletions webpack/scenes/SmartProxy/Content.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import SmartProxyContentTable from './SmartProxyContentTable';
import SmartProxyExpandableTable from './SmartProxyExpandableTable';

const Content = ({ smartProxyId }) => (
<SmartProxyContentTable smartProxyId={smartProxyId} />
<SmartProxyExpandableTable smartProxyId={smartProxyId} />
);

Content.propTypes = {
Expand Down
71 changes: 71 additions & 0 deletions webpack/scenes/SmartProxy/ExpandableCvDetails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
import { TableComposable, Thead, Tr, Th, Tbody, Td, TableVariant } from '@patternfly/react-table';
import { CheckCircleIcon, TimesCircleIcon } from '@patternfly/react-icons';
import LongDateTime from 'foremanReact/components/common/dates/LongDateTime';
import { urlBuilder } from 'foremanReact/common/urlHelpers';
import ContentViewIcon from '../ContentViews/components/ContentViewIcon';

const ExpandableCvDetails = ({ contentViews }) => {
const columnHeaders = [
__('Content view'),
__('Last published'),
__('Repositories'),
__('Synced to smart proxy'),
];

return (
<TableComposable
variant={TableVariant.compact}
aria-label="expandable-content-views"
ouiaId="expandable-content-views"
>
<Thead>
<Tr ouiaId="column-headers">
{columnHeaders.map(col => (
<Th
modifier="fitContent"
key={col}
>
{col}
</Th>
))}
</Tr>
</Thead>
<Tbody>
{contentViews.map((cv) => {
const {
id, name: cvName, composite, up_to_date: upToDate, counts,
} = cv;
const { repositories } = counts;
const upToDateVal = upToDate ? <CheckCircleIcon /> : <TimesCircleIcon />;
return (
<Tr key={cv.name} ouiaId={cv.name}>
<Td>
<ContentViewIcon
composite={composite}
description={<a href={cv.default ? urlBuilder('products', '') : urlBuilder('content_views', '', id)}>{cvName}</a>}
/>
</Td>
<Td><LongDateTime date={cv.last_published} showRelativeTimeTooltip /></Td>
<Td>{repositories}</Td>
<Td>{upToDateVal}</Td>
</Tr>
);
})}
</Tbody>
</TableComposable>

);
};

ExpandableCvDetails.propTypes = {
contentViews: PropTypes.arrayOf(PropTypes.shape({})),
};

ExpandableCvDetails.defaultProps = {
contentViews: [],
};

export default ExpandableCvDetails;
109 changes: 109 additions & 0 deletions webpack/scenes/SmartProxy/SmartProxyExpandableTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useState, useCallback } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
import { Thead, Tbody, Th, Tr, Td } from '@patternfly/react-table';
import getSmartProxyContent from './SmartProxyContentActions';
import {
selectSmartProxyContent,
selectSmartProxyContentStatus,
selectSmartProxyContentError,
} from './SmartProxyContentSelectors';
import { useSet } from '../../components/Table/TableHooks';
import TableWrapper from '../../components/Table/TableWrapper';
import ExpandableCvDetails from './ExpandableCvDetails';

const SmartProxyExpandableTable = ({ smartProxyId }) => {
const response = useSelector(selectSmartProxyContent);
const status = useSelector(selectSmartProxyContentStatus);
const error = useSelector(selectSmartProxyContentError);
const [searchQuery, updateSearchQuery] = useState('');
const expandedTableRows = useSet([]);
const tableRowIsExpanded = id => expandedTableRows.has(id);
let metadata = {};
const {
lifecycle_environments: results,
} = response;
if (results) {
metadata = { total: results.length, subtotal: results.length };
}
const columnHeaders = [
__('Environment'),
];
const fetchItems = useCallback(() => getSmartProxyContent({ smartProxyId }), [smartProxyId]);

const emptyContentTitle = __('No content views yet');
const emptyContentBody = __('You currently have no content views to display');
const emptySearchTitle = __('No matching content views found');
const emptySearchBody = __('Try changing your search settings.');
const alwaysHideToolbar = true;
const hidePagination = true;
return (
<TableWrapper
{...{
error,
metadata,
emptyContentTitle,
emptyContentBody,
emptySearchTitle,
emptySearchBody,
searchQuery,
updateSearchQuery,
fetchItems,
alwaysHideToolbar,
hidePagination,
}}
ouiaId="capsule-content-table"
autocompleteEndpoint=""
status={status}
>
<Thead>
<Tr ouiaId="cvTableHeaderRow">
<Th key="expand-carat" />
{columnHeaders.map(col => (
<Th
key={col}
>
{col}
</Th>
))}
</Tr>
</Thead>
{
results?.map((env, rowIndex) => {
const { name, id, content_views: contentViews } = env;
const isExpanded = tableRowIsExpanded(id);
return (
<Tbody isExpanded={isExpanded} key={id}>
<Tr key={id} ouiaId={`EnvRow-${id}`}>
<Td
expand={{
rowIndex,
isExpanded,
onToggle: (_event, _rInx, isOpen) =>
expandedTableRows.onToggle(isOpen, id),
}}
/>
<Td>{name}</Td>
</Tr>
<Tr key="child_row" ouiaId={`ContentViewTableRowChild-${id}`} isExpanded={isExpanded}>
<Td colSpan={2}>
<ExpandableCvDetails contentViews={contentViews} />
</Td>
</Tr>
</Tbody>
);
})
}
</TableWrapper >
);
};

SmartProxyExpandableTable.propTypes = {
smartProxyId: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string, // The API can sometimes return strings
]).isRequired,
};

export default SmartProxyExpandableTable;
15 changes: 7 additions & 8 deletions webpack/scenes/SmartProxy/__tests__/SmartProxyContentTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,28 @@ import { renderWithRedux, patientlyWaitFor } from 'react-testing-lib-wrapper';

import { nockInstance, assertNockRequest } from '../../../test-utils/nockWrapper';
import api from '../../../services/api';
import SmartProxyContentTable from '../SmartProxyContentTable';
import SmartProxyExpandableTable from '../SmartProxyExpandableTable';

const smartProxyContentData = require('./SmartProxyContentResult.fixtures.json');

const smartProxyContentPath = api.getApiUrl('/capsules/1/content/sync');

const smartProxyContent = { ...smartProxyContentData };

const contentTable = <SmartProxyContentTable smartProxyId={1} />;
const contentTable = <SmartProxyExpandableTable smartProxyId={1} />;

test('Can display Smart proxy content table', async (done) => {
const detailsScope = nockInstance
.get(smartProxyContentPath)
.query(true)
.reply(200, smartProxyContent);

const { getByText, getAllByLabelText } = renderWithRedux(contentTable);
const { getByText, getAllByText, getAllByLabelText } = renderWithRedux(contentTable);
await patientlyWaitFor(() => expect(getByText('Environment')).toBeInTheDocument());
expect(getByText('Content view')).toBeInTheDocument();
expect(getByText('Type')).toBeInTheDocument();
expect(getByText('Last published')).toBeInTheDocument();
expect(getByText('Repositories')).toBeInTheDocument();
expect(getByText('Synced to smart proxy')).toBeInTheDocument();
expect(getAllByText('Content view')[0]).toBeInTheDocument();
expect(getAllByText('Last published')[0]).toBeInTheDocument();
expect(getAllByText('Repositories')[0]).toBeInTheDocument();
expect(getAllByText('Synced to smart proxy')[0]).toBeInTheDocument();
expect(getAllByLabelText('Details')[0]).toHaveAttribute('aria-expanded', 'false');
getAllByLabelText('Details')[0].click();
expect(getAllByLabelText('Details')[0]).toHaveAttribute('aria-expanded', 'true');
Expand Down

0 comments on commit 434a00b

Please sign in to comment.