debian-image-builder-frontend/src/Components/ImagesTable/ImagesTable.js
regexowl 0bced556a9 ESLint: Use --fix with new rules to order import declarations
Related to #795. This applies the new sorting rules in ESLint to the files by running `npm run lint:js:fix`
2022-09-14 13:24:38 +02:00

322 lines
10 KiB
JavaScript

import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import {
ActionsColumn,
ExpandableRowContent,
TableComposable,
Tbody,
Td,
Th,
Thead,
Tr,
} from '@patternfly/react-table';
import {
EmptyState,
EmptyStateBody,
EmptyStateIcon,
EmptyStateSecondaryActions,
EmptyStateVariant,
Pagination,
PaginationVariant,
Title,
Toolbar,
ToolbarContent,
ToolbarItem,
} from '@patternfly/react-core';
import { PlusCircleIcon } from '@patternfly/react-icons';
import './ImagesTable.scss';
import ImageBuildStatus from './ImageBuildStatus';
import Release from './Release';
import Target from './Target';
import ImageLink from './ImageLink';
import ErrorDetails from './ImageBuildErrorDetails';
import DocumentationButton from '../sharedComponents/DocumentationButton';
import { composeGetStatus, composesGet } from '../../store/actions/actions';
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();
const navigate = useNavigate();
const pollComposeStatuses = () => {
Object.entries(composes.byId).map(([id, compose]) => {
/* Skip composes that have been complete */
if (
compose.image_status.status === 'success' ||
compose.image_status.status === 'failure'
) {
return;
}
dispatch(composeGetStatus(id));
});
};
useEffect(() => {
dispatch(composesGet(perPage, 0));
const intervalId = setInterval(() => pollComposeStatuses(), 8000);
// clean up interval on unmount
return () => clearInterval(intervalId);
}, []);
const onSetPage = (_, page) => {
// if the next page's composes haven't been fetched from api yet
// then fetch them with proper page index and offset
if (composes.count > composes.allIds.length) {
const pageIndex = page - 1;
const offset = pageIndex * perPage;
dispatch(composesGet(perPage, offset));
}
setPage(page);
};
const onPerPageSelect = (_, perPage) => {
// if the new per page quantity is greater than the number of already fetched composes fetch more composes
// if all composes haven't already been fetched
if (
composes.count > composes.allIds.length &&
perPage > composes.allIds.length
) {
dispatch(composesGet(perPage, 0));
}
// page should be reset to the first page when the page size is changed.
setPerPage(perPage);
setPage(1);
};
const timestampToDisplayString = (ts) => {
// timestamp has format 2021-04-27 12:31:12.794809 +0000 UTC
// must be converted to ms timestamp and then reformatted to Apr 27, 2021
if (!ts) {
return '';
}
// get YYYY-MM-DD format
const date = ts.slice(0, 10);
const ms = Date.parse(date);
const options = { month: 'short', day: 'numeric', year: 'numeric' };
const tsDisplay = new Intl.DateTimeFormat('en-US', options).format(ms);
return tsDisplay;
};
const actions = (compose) => [
{
title: 'Recreate image',
onClick: () =>
navigate('/imagewizard', {
state: { composeRequest: compose.request, initialStep: 'review' },
}),
},
{
title: (
<a
href={`data:text/plain;charset=utf-8,${encodeURIComponent(
JSON.stringify(compose.request)
)}`}
download="request.json"
>
Download compose request (.json)
</a>
),
},
];
// the state.page is not an index so must be reduced by 1 get the starting index
const itemsStartInclusive = (page - 1) * perPage;
const itemsEndExclusive = itemsStartInclusive + perPage;
return (
<React.Fragment>
{(composes.allIds.length === 0 && (
<EmptyState variant={EmptyStateVariant.large} data-testid="empty-state">
<EmptyStateIcon icon={PlusCircleIcon} />
<Title headingLevel="h4" size="lg">
Create an image
</Title>
<EmptyStateBody>
Create OS images for deployment in Amazon Web Services, Microsoft
Azure and Google Cloud Platform. Images can include a custom package
set and an activation key to automate the registration process.
</EmptyStateBody>
<Link
to="/imagewizard"
className="pf-c-button pf-m-primary"
data-testid="create-image-action"
>
Create image
</Link>
<EmptyStateSecondaryActions>
<DocumentationButton />
</EmptyStateSecondaryActions>
</EmptyState>
)) || (
<React.Fragment>
<Toolbar>
<ToolbarContent>
<ToolbarItem>
<Link
to="/imagewizard"
className="pf-c-button pf-m-primary"
data-testid="create-image-action"
>
Create image
</Link>
</ToolbarItem>
<ToolbarItem
variant="pagination"
align={{ default: 'alignRight' }}
>
<Pagination
itemCount={composes.count}
perPage={perPage}
page={page}
onSetPage={onSetPage}
onPerPageSelect={onPerPageSelect}
widgetId="compose-pagination-top"
data-testid="images-pagination-top"
isCompact
/>
</ToolbarItem>
</ToolbarContent>
</Toolbar>
<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>
<ErrorDetails status={compose.image_status} />
</ExpandableRowContent>
</Td>
</Tr>
</Tbody>
);
})}
</TableComposable>
<Toolbar>
<ToolbarContent>
<ToolbarItem
variant="pagination"
align={{ default: 'alignRight' }}
>
<Pagination
variant={PaginationVariant.bottom}
itemCount={composes.count}
perPage={perPage}
page={page}
onSetPage={onSetPage}
onPerPageSelect={onPerPageSelect}
widgetId="compose-pagination-bottom"
data-testid="images-pagination-bottom"
isCompact
/>
</ToolbarItem>
</ToolbarContent>
</Toolbar>
</React.Fragment>
)}
</React.Fragment>
);
};
ImagesTable.propTypes = {
composes: PropTypes.object,
composesGet: PropTypes.func,
composeGetStatus: PropTypes.func,
};
export default ImagesTable;