Merge pull request #9932 from marshmalien/5070-expanded-project-list

Add expanded row content to project list

SUMMARY
#5070
Add the following details to expanded area:

Description
Organization
Execution Environment
Last modified
Last used


ISSUE TYPE


Feature Pull Request

COMPONENT NAME


UI

Reviewed-by: Kersom <None>
Reviewed-by: Tiago Góes <tiago.goes2009@gmail.com>
This commit is contained in:
softwarefactory-project-zuul[bot] 2021-04-20 21:10:15 +00:00 committed by GitHub
commit ddcbef8545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 227 additions and 98 deletions

View File

@ -170,7 +170,7 @@ function ProjectList({ i18n }) {
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
<HeaderCell sortKey="name">{i18n._(t`Name`)}</HeaderCell>
<HeaderCell>{i18n._(t`Status`)}</HeaderCell>
<HeaderCell>{i18n._(t`Type`)}</HeaderCell>

View File

@ -3,7 +3,7 @@ import React, { Fragment, useState, useCallback } from 'react';
import { string, bool, func } from 'prop-types';
import { withI18n } from '@lingui/react';
import { Button, Tooltip } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table';
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
import { t } from '@lingui/macro';
import { Link } from 'react-router-dom';
import {
@ -15,6 +15,12 @@ import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
import { formatDateString, timeOfDay } from '../../../util/dates';
import { ProjectsAPI } from '../../../api';
import ClipboardCopyButton from '../../../components/ClipboardCopyButton';
import {
DetailList,
Detail,
DeletedDetail,
} from '../../../components/DetailList';
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
import StatusLabel from '../../../components/StatusLabel';
import { toTitleCase } from '../../../util/strings';
import CopyButton from '../../../components/CopyButton';
@ -39,6 +45,7 @@ function ProjectListItem({
rowIndex,
i18n,
}) {
const [isExpanded, setIsExpanded] = useState(false);
const [isDisabled, setIsDisabled] = useState(false);
ProjectListItem.propTypes = {
project: Project.isRequired,
@ -87,104 +94,159 @@ function ProjectListItem({
project.custom_virtualenv && !project.default_environment;
return (
<Tr id={`${project.id}`}>
<Td
select={{
rowIndex,
isSelected,
onSelect,
}}
dataLabel={i18n._(t`Selected`)}
/>
<Td id={labelId} dataLabel={i18n._(t`Name`)}>
<span>
<Link to={`${detailUrl}`}>
<b>{project.name}</b>
</Link>
</span>
{missingExecutionEnvironment && (
<span>
<Tooltip
content={i18n._(
t`Custom virtual environment ${project.custom_virtualenv} must be replaced by an execution environment.`
)}
position="right"
className="missing-execution-environment"
>
<ExclamationTriangleIcon />
</Tooltip>
</span>
)}
</Td>
<Td dataLabel={i18n._(t`Status`)}>
{project.summary_fields.last_job && (
<Tooltip
position="top"
content={generateLastJobTooltip(project.summary_fields.last_job)}
key={project.summary_fields.last_job.id}
>
<Link to={`/jobs/project/${project.summary_fields.last_job.id}`}>
<StatusLabel status={project.summary_fields.last_job.status} />
</Link>
</Tooltip>
)}
</Td>
<Td dataLabel={i18n._(t`Type`)}>
{project.scm_type === ''
? i18n._(t`Manual`)
: toTitleCase(project.scm_type)}
</Td>
<Td dataLabel={i18n._(t`Revision`)}>
{project.scm_revision.substring(0, 7)}
{!project.scm_revision && (
<Label aria-label={i18n._(t`copy to clipboard disabled`)}>
{i18n._(t`Sync for revision`)}
</Label>
)}
<ClipboardCopyButton
isDisabled={!project.scm_revision}
stringToCopy={project.scm_revision}
copyTip={i18n._(t`Copy full revision to clipboard.`)}
copiedSuccessTip={i18n._(t`Successfully copied to clipboard!`)}
ouiaId="copy-revision-button"
<>
<Tr id={`${project.id}`}>
<Td
expand={{
rowIndex,
isExpanded,
onToggle: () => setIsExpanded(!isExpanded),
}}
/>
</Td>
<ActionsTd dataLabel={i18n._(t`Actions`)}>
<ActionItem
visible={project.summary_fields.user_capabilities.start}
tooltip={i18n._(t`Sync Project`)}
>
<ProjectSyncButton projectId={project.id} />
</ActionItem>
<ActionItem
visible={project.summary_fields.user_capabilities.edit}
tooltip={i18n._(t`Edit Project`)}
>
<Button
ouiaId={`${project.id}-edit-button`}
isDisabled={isDisabled}
aria-label={i18n._(t`Edit Project`)}
variant="plain"
component={Link}
to={`/projects/${project.id}/edit`}
>
<PencilAltIcon />
</Button>
</ActionItem>
<ActionItem
tooltip={i18n._(t`Copy Project`)}
visible={project.summary_fields.user_capabilities.copy}
>
<CopyButton
copyItem={copyProject}
isDisabled={isDisabled}
onCopyStart={handleCopyStart}
onCopyFinish={handleCopyFinish}
errorMessage={i18n._(t`Failed to copy project.`)}
<Td
select={{
rowIndex,
isSelected,
onSelect,
}}
dataLabel={i18n._(t`Selected`)}
/>
<Td id={labelId} dataLabel={i18n._(t`Name`)}>
<span>
<Link to={`${detailUrl}`}>
<b>{project.name}</b>
</Link>
</span>
{missingExecutionEnvironment && (
<span>
<Tooltip
content={i18n._(
t`Custom virtual environment ${project.custom_virtualenv} must be replaced by an execution environment.`
)}
position="right"
className="missing-execution-environment"
>
<ExclamationTriangleIcon />
</Tooltip>
</span>
)}
</Td>
<Td dataLabel={i18n._(t`Status`)}>
{project.summary_fields.last_job && (
<Tooltip
position="top"
content={generateLastJobTooltip(project.summary_fields.last_job)}
key={project.summary_fields.last_job.id}
>
<Link to={`/jobs/project/${project.summary_fields.last_job.id}`}>
<StatusLabel status={project.summary_fields.last_job.status} />
</Link>
</Tooltip>
)}
</Td>
<Td dataLabel={i18n._(t`Type`)}>
{project.scm_type === ''
? i18n._(t`Manual`)
: toTitleCase(project.scm_type)}
</Td>
<Td dataLabel={i18n._(t`Revision`)}>
{project.scm_revision.substring(0, 7)}
{!project.scm_revision && (
<Label aria-label={i18n._(t`copy to clipboard disabled`)}>
{i18n._(t`Sync for revision`)}
</Label>
)}
<ClipboardCopyButton
isDisabled={!project.scm_revision}
stringToCopy={project.scm_revision}
copyTip={i18n._(t`Copy full revision to clipboard.`)}
copiedSuccessTip={i18n._(t`Successfully copied to clipboard!`)}
ouiaId="copy-revision-button"
/>
</ActionItem>
</ActionsTd>
</Tr>
</Td>
<ActionsTd dataLabel={i18n._(t`Actions`)}>
<ActionItem
visible={project.summary_fields.user_capabilities.start}
tooltip={i18n._(t`Sync Project`)}
>
<ProjectSyncButton projectId={project.id} />
</ActionItem>
<ActionItem
visible={project.summary_fields.user_capabilities.edit}
tooltip={i18n._(t`Edit Project`)}
>
<Button
ouiaId={`${project.id}-edit-button`}
isDisabled={isDisabled}
aria-label={i18n._(t`Edit Project`)}
variant="plain"
component={Link}
to={`/projects/${project.id}/edit`}
>
<PencilAltIcon />
</Button>
</ActionItem>
<ActionItem
tooltip={i18n._(t`Copy Project`)}
visible={project.summary_fields.user_capabilities.copy}
>
<CopyButton
copyItem={copyProject}
isDisabled={isDisabled}
onCopyStart={handleCopyStart}
onCopyFinish={handleCopyFinish}
errorMessage={i18n._(t`Failed to copy project.`)}
/>
</ActionItem>
</ActionsTd>
</Tr>
<Tr isExpanded={isExpanded} id={`expanded-project-row-${project.id}`}>
<Td colSpan={2} />
<Td colSpan={5}>
<ExpandableRowContent>
<DetailList>
<Detail
label={i18n._(t`Description`)}
value={project.description}
dataCy={`project-${project.id}-description`}
/>
{project.summary_fields.organization ? (
<Detail
label={i18n._(t`Organization`)}
value={
<Link
to={`/organizations/${project.summary_fields.organization.id}/details`}
>
{project.summary_fields.organization.name}
</Link>
}
dataCy={`project-${project.id}-organization`}
/>
) : (
<DeletedDetail label={i18n._(t`Organization`)} />
)}
<ExecutionEnvironmentDetail
virtualEnvironment={project.custom_virtualenv}
executionEnvironment={
project.summary_fields?.default_environment
}
isDefaultEnvironment
/>
<Detail
label={i18n._(t`Last modified`)}
value={formatDateString(project.modified)}
dataCy={`project-${project.id}-last-modified`}
/>
<Detail
label={i18n._(t`Last used`)}
value={formatDateString(project.last_job_run)}
dataCy={`project-${project.id}-last-used`}
/>
</DetailList>
</ExpandableRowContent>
</Td>
</Tr>
</>
);
}
export default withI18n()(ProjectListItem);

View File

@ -319,4 +319,71 @@ describe('<ProjectsListItem />', () => {
).toBe('Sync for revision');
expect(wrapper.find('ClipboardCopyButton').prop('isDisabled')).toBe(true);
});
test('should render expected details in expanded section', async () => {
const wrapper = mountWithContexts(
<table>
<tbody>
<ProjectsListItem
rowIndex={1}
isSelected={false}
detailUrl="/project/1"
onSelect={() => {}}
project={{
id: 1,
name: 'Project 1',
description: 'Project 1 description',
url: '/api/v2/projects/1',
type: 'project',
scm_type: 'git',
scm_revision: '123456789',
summary_fields: {
organization: {
id: 999,
description: '',
name: 'Mock org',
},
user_capabilities: {
start: true,
},
default_environment: {
id: 123,
name: 'Mock EE',
image: 'mock.image',
},
},
custom_virtualenv: '/var/lib/awx/env',
default_environment: 123,
organization: 999,
}}
/>
</tbody>
</table>
);
expect(
wrapper
.find('Tr')
.last()
.prop('isExpanded')
).toBe(false);
await act(async () =>
wrapper.find('button[aria-label="Details"]').simulate('click')
);
wrapper.update();
expect(
wrapper
.find('Tr')
.last()
.prop('isExpanded')
).toBe(true);
function assertDetail(label, value) {
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
}
assertDetail('Description', 'Project 1 description');
assertDetail('Organization', 'Mock org');
assertDetail('Default Execution Environment', 'Mock EE');
expect(wrapper.find('Detail[label="Last modified"]').length).toBe(1);
expect(wrapper.find('Detail[label="Last used"]').length).toBe(1);
});
});