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:
parent
110c0c674b
commit
d7035d544b
3 changed files with 105 additions and 62 deletions
|
|
@ -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>
|
||||
|
|
|
|||
10
src/Components/ImagesTable/ImagesTable.scss
Normal file
10
src/Components/ImagesTable/ImagesTable.scss
Normal 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;
|
||||
}
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue