mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 10:30:03 -03:30
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:
commit
ddcbef8545
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user