Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #36720 - Composable Expandable Table for Smart Proxy content #10716

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading