ImagesTable: Show UUID in expandable details row

This commit displays the UUID in an expandable details row. This is
necessary because if an image was named, the UUID was not displayed.
It is important that a user know the UUID for troubleshooting, for
example in the case of requesting help with an image.

To facilitate this, the original Table component was converted to a
TableComposable component. TableComposable is newer and recommended over
the older Table by PatternFly.
This commit is contained in:
lucasgarfield 2022-04-08 12:05:52 +02:00 committed by Sanne Raymaekers
parent 110c0c674b
commit d7035d544b
3 changed files with 105 additions and 62 deletions

View file

@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { Table, TableHeader, TableBody } from '@patternfly/react-table';
import { TableComposable, Thead, Tr, Th, Tbody, Td, ActionsColumn, ExpandableRowContent } from '@patternfly/react-table';
import { EmptyState, EmptyStateVariant, EmptyStateIcon, EmptyStateBody, EmptyStateSecondaryActions,
Pagination,
Toolbar, ToolbarContent, ToolbarItem,
Title } from '@patternfly/react-core';
import { PlusCircleIcon } from '@patternfly/react-icons';
import './ImagesTable.scss';
import { composesGet, composeGetStatus } from '../../store/actions/actions';
import DocumentationButton from '../sharedComponents/DocumentationButton';
import ImageBuildStatus from './ImageBuildStatus';
@ -20,6 +20,16 @@ const ImagesTable = () => {
const [ page, setPage ] = useState(1);
const [ perPage, setPerPage ] = useState(10);
const [ expandedComposeIds, setExpandedComposeIds ] = useState([]);
const isExpanded = compose => expandedComposeIds.includes(compose.id);
const handleToggle = (compose, isExpanding) => {
if (isExpanding) {
setExpandedComposeIds([ ...expandedComposeIds, compose.id ]);
} else {
setExpandedComposeIds(expandedComposeIds.filter(id => id !== compose.id));
}};
const composes = useSelector((state) => state.composes);
const dispatch = useDispatch();
@ -83,49 +93,19 @@ const ImagesTable = () => {
return tsDisplay;
};
const columns = [
'Image name',
'Created',
'Release',
'Target',
'Status',
'Instance',
''
const actions = (compose) => [
{
title: 'Recreate image',
onClick: () => navigate(
'/imagewizard',
{ state: { composeRequest: compose.request, initialStep: 'review' }}
)
}
];
// the state.page is not an index so must be reduced by 1 get the starting index
const itemsStartInclusive = (page - 1) * perPage;
const itemsEndExlcusive = itemsStartInclusive + perPage;
// only display the current pages section of composes. slice is inclusive, exclusive.
const rows = composes.allIds.slice(itemsStartInclusive, itemsEndExlcusive).map(id => {
const compose = composes.byId[id];
return {
compose,
cells: [
compose.request.image_name || id,
timestampToDisplayString(compose.created_at),
{ title: <Release release={ compose.request.distribution } /> },
{ title: <Target
uploadType={ compose.request.image_requests[0].upload_request.type }
imageType={ compose.request.image_requests[0].image_type } /> },
{ title: <ImageBuildStatus status={ compose.image_status ? compose.image_status.status : '' } /> },
{ title: <ImageLink
imageStatus={ compose.image_status }
imageType={ compose.request.image_requests[0].image_type }
uploadOptions={ compose.request.image_requests[0].upload_request.options } /> }
]
};
});
const actions = [
{
title: 'Recreate image',
onClick: (_event, _rowId, rowData) => navigate(
'/imagewizard',
{ state: { composeRequest: rowData.compose.request, initialStep: 'review' }}
)
}
];
const itemsEndExclusive = itemsStartInclusive + perPage;
return (
<React.Fragment>
@ -170,15 +150,52 @@ const ImagesTable = () => {
</ToolbarItem>
</ToolbarContent>
</Toolbar>
<Table
aria-label="Images"
rows={ rows }
cells={ columns }
actions={ actions }
data-testid="images-table">
<TableHeader />
<TableBody />
</Table>
<TableComposable variant="compact" data-testid="images-table">
<Thead>
<Tr>
<Th />
<Th>Image name</Th>
<Th>Created</Th>
<Th>Release</Th>
<Th>Target</Th>
<Th>Status</Th>
<Th>Instance</Th>
<Th />
</Tr>
</Thead>
{ composes.allIds.slice(itemsStartInclusive, itemsEndExclusive).map((id, rowIndex) => {
const compose = composes.byId[id];
return (
<Tbody key={ id } isExpanded={ isExpanded(compose) }>
<Tr>
<Td expand={ { rowIndex, isExpanded: isExpanded(compose),
onToggle: () => handleToggle(compose, !isExpanded(compose)) } } />
<Td dataLabel="Image name">{compose.request.image_name || id}</Td>
<Td dataLabel="Created">{timestampToDisplayString(compose.created_at)}</Td>
<Td dataLabel="Release"><Release release={ compose.request.distribution } /></Td>
<Td dataLabel="Target"><Target
uploadType={ compose.request.image_requests[0].upload_request.type }
imageType={ compose.request.image_requests[0].image_type } /></Td>
<Td dataLabel="Status"><ImageBuildStatus
status={ compose.image_status ? compose.image_status.status : '' } /></Td>
<Td dataLabel="Instance"><ImageLink
imageStatus={ compose.image_status }
imageType={ compose.request.image_requests[0].image_type }
uploadOptions={ compose.request.image_requests[0].upload_request.options } /></Td>
<Td><ActionsColumn items={ actions(compose) } /></Td>
</Tr>
<Tr isExpanded={ isExpanded(compose) }>
<Td colSpan={ 8 }>
<ExpandableRowContent>
<strong>UUID</strong>
<div>{ id }</div>
</ExpandableRowContent>
</Td>
</Tr>
</Tbody>
);
}) }
</TableComposable>
</React.Fragment>
)}
</React.Fragment>

View file

@ -0,0 +1,10 @@
@media only screen and (min-width: 768px) {
.pf-c-table__expandable-row td:first-child {
// Align with expand/collapse button by duplicating its padding
padding-left: calc(var(--pf-c-table--cell--first-last-child--PaddingLeft) + var(--pf-global--spacer--md));
}
}
.pf-m-expanded tr:first-child {
// Remove border between a compose and its expanded detail
border-bottom-style: none;
}

View file

@ -364,17 +364,17 @@ describe('Images Table', () => {
// remove first row from list since it is just header labels
const header = rows.shift();
// test the header has correct labels
expect(header.cells[0]).toHaveTextContent('Image name');
expect(header.cells[1]).toHaveTextContent('Created');
expect(header.cells[2]).toHaveTextContent('Release');
expect(header.cells[3]).toHaveTextContent('Target');
expect(header.cells[4]).toHaveTextContent('Status');
expect(header.cells[5]).toHaveTextContent('Instance');
expect(header.cells[1]).toHaveTextContent('Image name');
expect(header.cells[2]).toHaveTextContent('Created');
expect(header.cells[3]).toHaveTextContent('Release');
expect(header.cells[4]).toHaveTextContent('Target');
expect(header.cells[5]).toHaveTextContent('Status');
expect(header.cells[6]).toHaveTextContent('Instance');
// 10 rows for 10 images
expect(rows).toHaveLength(10);
for (const row of rows) {
const col1 = row.cells[0].textContent;
const col1 = row.cells[1].textContent;
const composes = Object.values(store.composes.byId);
// find compose with either the user defined image name or the uuid
@ -382,25 +382,25 @@ describe('Images Table', () => {
expect(compose).toBeTruthy();
// date should match the month day and year of the timestamp.
expect(row.cells[1]).toHaveTextContent('Apr 27, 2021');
expect(row.cells[2]).toHaveTextContent('Apr 27, 2021');
// render the expected <ImageBuildStatus /> and compare the text content
let testElement = document.createElement('testElement');
render(<Target
imageType={ compose.request.image_requests[0].image_type }
uploadType={ compose.request.image_requests[0].upload_request.type } />, { container: testElement });
expect(row.cells[3]).toHaveTextContent(testElement.textContent);
expect(row.cells[4]).toHaveTextContent(testElement.textContent);
// render the expected <ImageBuildStatus /> and compare the text content
render(<ImageBuildStatus status={ compose.image_status.status } />, { container: testElement });
expect(row.cells[4]).toHaveTextContent(testElement.textContent);
expect(row.cells[5]).toHaveTextContent(testElement.textContent);
// render the expected <ImageLink /> and compare the text content for a link
render(
<ImageLink imageStatus={ compose.image_status } uploadOptions={ compose.request.image_requests[0].upload_request.options } />,
{ container: testElement }
);
expect(row.cells[5]).toHaveTextContent(testElement.textContent);
expect(row.cells[6]).toHaveTextContent(testElement.textContent);
}
});
@ -413,7 +413,7 @@ describe('Images Table', () => {
const rows = getAllByRole('row');
// first row is header so look at index 1
const imageId = rows[1].cells[0].textContent;
const imageId = rows[1].cells[1].textContent;
const actionsButton = within(rows[1]).getByRole('button', {
name: 'Actions'
@ -428,6 +428,22 @@ describe('Images Table', () => {
expect(history.location.state.composeRequest).toStrictEqual(store.composes.byId[imageId].request);
expect(history.location.state.initialStep).toBe('review');
});
test('check expandable row toggle', () => {
renderWithReduxRouter(<ImagesTable />, store);
const table = screen.getByTestId('images-table');
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
const toggleButton = within(rows[6]).getByRole('button', { name: /details/i });
expect(screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]).not.toBeVisible();
userEvent.click(toggleButton);
expect(screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]).toBeVisible();
userEvent.click(toggleButton);
expect(screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]).not.toBeVisible();
});
});
describe('Images Table Toolbar', () => {