Merge pull request #5616 from marshmalien/5541-reuse-ActionButtonWrapper

Update Detail views to use CardActionsRow

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-01-09 21:52:29 +00:00 committed by GitHub
commit 3e58ee068c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 162 additions and 245 deletions

View File

@ -133,7 +133,7 @@ class Host extends Component {
{host && (
<Route
path="/hosts/:id/details"
render={() => <HostDetail match={match} host={host} />}
render={() => <HostDetail host={host} />}
/>
)}
{host && (

View File

@ -1,23 +1,13 @@
import React from 'react';
import { Link, withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import { Host } from '@types';
import { Button } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import { CardBody, CardActionsRow } from '@components/Card';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import { VariablesDetail } from '@components/CodeMirrorInput';
const ActionButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 20px;
& > :not(:first-child) {
margin-left: 20px;
}
`;
function HostDetail({ host, i18n }) {
const { created, description, id, modified, name, summary_fields } = host;
@ -58,7 +48,7 @@ function HostDetail({ host, i18n }) {
rows={6}
/>
</DetailList>
<ActionButtonWrapper>
<CardActionsRow>
{summary_fields.user_capabilities &&
summary_fields.user_capabilities.edit && (
<Button
@ -69,7 +59,7 @@ function HostDetail({ host, i18n }) {
{i18n._(t`Edit`)}
</Button>
)}
</ActionButtonWrapper>
</CardActionsRow>
</CardBody>
);
}
@ -78,4 +68,4 @@ HostDetail.propTypes = {
host: Host.isRequired,
};
export default withI18n()(withRouter(HostDetail));
export default withI18n()(HostDetail);

View File

@ -16,9 +16,11 @@ GroupsAPI.readDetail.mockResolvedValue({
variables: 'bizz: buzz',
summary_fields: {
inventory: { id: 1 },
created_by: { id: 1, name: 'Athena' },
modified_by: { id: 1, name: 'Apollo' },
created_by: { id: 1, username: 'Athena' },
modified_by: { id: 1, username: 'Apollo' },
},
created: '2020-04-25T01:23:45.678901Z',
modified: '2020-04-25T01:23:45.678901Z',
},
});
describe('<InventoryGroup />', () => {

View File

@ -3,27 +3,16 @@ import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { withRouter } from 'react-router-dom';
import styled from 'styled-components';
import { useHistory, useParams } from 'react-router-dom';
import { VariablesDetail } from '@components/CodeMirrorInput';
import { CardBody } from '@components/Card';
import { CardBody, CardActionsRow } from '@components/Card';
import ErrorDetail from '@components/ErrorDetail';
import AlertModal from '@components/AlertModal';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
import { GroupsAPI, InventoriesAPI } from '@api';
// TODO: extract this into a component for use in all relevant Detail views
const ActionButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 20px;
& > :not(:first-child) {
margin-left: 20px;
}
`;
function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
function InventoryGroupDetail({ i18n, inventoryGroup }) {
const {
summary_fields: { created_by, modified_by },
created,
@ -34,15 +23,21 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
} = inventoryGroup;
const [error, setError] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const history = useHistory();
const params = useParams();
const handleDelete = async option => {
const inventoryId = parseInt(params.id, 10);
const groupId = parseInt(params.groupId, 10);
setIsDeleteModalOpen(false);
try {
if (option === 'delete') {
await GroupsAPI.destroy(inventoryGroup.id);
await GroupsAPI.destroy(groupId);
} else {
await InventoriesAPI.promoteGroup(match.params.id, inventoryGroup.id);
await InventoriesAPI.promoteGroup(inventoryId, groupId);
}
history.push(`/inventories/inventory/${match.params.id}/groups`);
history.push(`/inventories/inventory/${inventoryId}/groups`);
} catch (err) {
setError(err);
}
@ -69,13 +64,13 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
user={modified_by}
/>
</DetailList>
<ActionButtonWrapper>
<CardActionsRow>
<Button
variant="primary"
aria-label={i18n._(t`Edit`)}
onClick={() =>
history.push(
`/inventories/inventory/${match.params.id}/groups/${inventoryGroup.id}/edit`
`/inventories/inventory/${params.id}/groups/${params.groupId}/edit`
)
}
>
@ -88,7 +83,7 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
>
{i18n._(t`Delete`)}
</Button>
</ActionButtonWrapper>
</CardActionsRow>
{isDeleteModalOpen && (
<InventoryGroupsDeleteModal
groups={[inventoryGroup]}
@ -111,4 +106,4 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
</CardBody>
);
}
export default withI18n()(withRouter(InventoryGroupDetail));
export default withI18n()(InventoryGroupDetail);

View File

@ -2,13 +2,12 @@ import React from 'react';
import { GroupsAPI } from '@api';
import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import InventoryGroupDetail from './InventoryGroupDetail';
jest.mock('@api');
const inventoryGroup = {
name: 'Foo',
description: 'Bar',
@ -27,6 +26,7 @@ const inventoryGroup = {
},
},
};
describe('<InventoryGroupDetail />', () => {
let wrapper;
let history;
@ -86,7 +86,7 @@ describe('<InventoryGroupDetail />', () => {
'/inventories/inventory/1/groups/1/edit'
);
});
test('details shoudld render with the proper values', () => {
test('details should render with the proper values', () => {
expect(wrapper.find('Detail[label="Name"]').prop('value')).toBe('Foo');
expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe(
'Bar'

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { Link, useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core';
@ -7,9 +7,10 @@ import styled from 'styled-components';
import AlertModal from '@components/AlertModal';
import { DetailList, Detail } from '@components/DetailList';
import { CardBody } from '@components/Card';
import { CardBody, CardActionsRow } from '@components/Card';
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
import { VariablesInput as _VariablesInput } from '@components/CodeMirrorInput';
import DeleteButton from '@components/DeleteButton';
import ErrorDetail from '@components/ErrorDetail';
import LaunchButton from '@components/LaunchButton';
import { StatusIcon } from '@components/Sparkline';
@ -24,16 +25,6 @@ import {
InventoriesAPI,
AdHocCommandsAPI,
} from '@api';
import { JOB_TYPE_URL_SEGMENTS } from '../../../constants';
const ActionButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 20px;
& > :not(:first-child) {
margin-left: 20px;
}
`;
const VariablesInput = styled(_VariablesInput)`
.pf-c-form__label {
@ -86,7 +77,7 @@ const getLaunchedByDetails = ({ summary_fields = {}, related = {} }) => {
return { link, value };
};
function JobDetail({ job, i18n, history }) {
function JobDetail({ job, i18n }) {
const {
credentials,
instance_group: instanceGroup,
@ -95,8 +86,8 @@ function JobDetail({ job, i18n, history }) {
labels,
project,
} = job.summary_fields;
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [errorMsg, setErrorMsg] = useState();
const history = useHistory();
const { value: launchedByValue, link: launchedByLink } =
getLaunchedByDetails(job) || {};
@ -125,7 +116,6 @@ function JobDetail({ job, i18n, history }) {
history.push('/jobs');
} catch (err) {
setErrorMsg(err);
setIsDeleteModalOpen(false);
}
};
@ -262,7 +252,7 @@ function JobDetail({ job, i18n, history }) {
label={i18n._(t`Artifacts`)}
/>
)}
<ActionButtonWrapper>
<CardActionsRow>
{job.type !== 'system_job' &&
job.summary_fields.user_capabilities.start && (
<LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}>
@ -274,45 +264,15 @@ function JobDetail({ job, i18n, history }) {
</LaunchButton>
)}
{job.summary_fields.user_capabilities.delete && (
<Button
variant="danger"
aria-label={i18n._(t`Delete`)}
onClick={() => setIsDeleteModalOpen(true)}
<DeleteButton
name={job.name}
modalTitle={i18n._(t`Delete Job`)}
onConfirm={deleteJob}
>
{i18n._(t`Delete`)}
</Button>
</DeleteButton>
)}
</ActionButtonWrapper>
{isDeleteModalOpen && (
<AlertModal
isOpen={isDeleteModalOpen}
title={i18n._(t`Delete Job`)}
variant="danger"
onClose={() => setIsDeleteModalOpen(false)}
>
{i18n._(t`Are you sure you want to delete:`)}
<br />
<strong>{job.name}</strong>
<ActionButtonWrapper>
<Button
variant="secondary"
aria-label={i18n._(t`Close`)}
component={Link}
to={`/jobs/${JOB_TYPE_URL_SEGMENTS[job.type]}/${job.id}`}
>
{i18n._(t`Cancel`)}
</Button>
<Button
variant="danger"
aria-label={i18n._(t`Delete`)}
onClick={deleteJob}
>
{i18n._(t`Delete`)}
</Button>
</ActionButtonWrapper>
</AlertModal>
)}
</CardActionsRow>
{errorMsg && (
<AlertModal
isOpen={errorMsg}
@ -330,4 +290,4 @@ JobDetail.propTypes = {
job: Job.isRequired,
};
export default withI18n()(withRouter(JobDetail));
export default withI18n()(JobDetail);

View File

@ -5,7 +5,7 @@ import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core';
import { OrganizationsAPI } from '@api';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import { CardBody } from '@components/Card';
import { CardBody, CardActionsRow } from '@components/Card';
import { ChipGroup, Chip } from '@components/Chip';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
@ -92,13 +92,13 @@ function OrganizationDetail({ i18n, organization }) {
/>
)}
</DetailList>
{summary_fields.user_capabilities.edit && (
<div css="margin-top: 10px; text-align: right;">
<CardActionsRow>
{summary_fields.user_capabilities.edit && (
<Button component={Link} to={`/organizations/${id}/edit`}>
{i18n._(t`Edit`)}
</Button>
</div>
)}
)}
</CardActionsRow>
</CardBody>
);
}

View File

@ -205,7 +205,7 @@ class Project extends Component {
{project && (
<Route
path="/projects/:id/details"
render={() => <ProjectDetail match={match} project={project} />}
render={() => <ProjectDetail project={project} />}
/>
)}
{project && (

View File

@ -1,25 +1,15 @@
import React from 'react';
import { Link, withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import { Project } from '@types';
import { Config } from '@contexts/Config';
import { Button, List, ListItem } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import { CardBody, CardActionsRow } from '@components/Card';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import { CredentialChip } from '@components/Chip';
import { toTitleCase } from '@util/strings';
const ActionButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 20px;
& > :not(:first-child) {
margin-left: 20px;
}
`;
function ProjectDetail({ project, i18n }) {
const {
allow_override,
@ -137,7 +127,7 @@ function ProjectDetail({ project, i18n }) {
user={summary_fields.modified_by}
/>
</DetailList>
<ActionButtonWrapper>
<CardActionsRow>
{summary_fields.user_capabilities &&
summary_fields.user_capabilities.edit && (
<Button
@ -148,7 +138,7 @@ function ProjectDetail({ project, i18n }) {
{i18n._(t`Edit`)}
</Button>
)}
</ActionButtonWrapper>
</CardActionsRow>
</CardBody>
);
}
@ -157,4 +147,4 @@ ProjectDetail.propTypes = {
project: Project.isRequired,
};
export default withI18n()(withRouter(ProjectDetail));
export default withI18n()(ProjectDetail);

View File

@ -125,7 +125,7 @@ class Team extends Component {
{team && (
<Route
path="/teams/:id/details"
render={() => <TeamDetail match={match} team={team} />}
render={() => <TeamDetail team={team} />}
/>
)}
{team && (

View File

@ -1,57 +1,49 @@
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import { CardBody, CardActionsRow } from '@components/Card';
import { DetailList, Detail } from '@components/DetailList';
import { formatDateString } from '@util/dates';
class TeamDetail extends Component {
render() {
const {
team: { name, description, created, modified, summary_fields },
match,
i18n,
} = this.props;
function TeamDetail({ team, i18n }) {
const { name, description, created, modified, summary_fields } = team;
const { id } = useParams();
return (
<CardBody>
<DetailList>
<Detail
label={i18n._(t`Name`)}
value={name}
dataCy="team-detail-name"
/>
<Detail label={i18n._(t`Description`)} value={description} />
<Detail
label={i18n._(t`Organization`)}
value={
<Link to={`/organizations/${summary_fields.organization.id}`}>
{summary_fields.organization.name}
</Link>
}
/>
<Detail
label={i18n._(t`Created`)}
value={formatDateString(created)}
/>
<Detail
label={i18n._(t`Last Modified`)}
value={formatDateString(modified)}
/>
</DetailList>
return (
<CardBody>
<DetailList>
<Detail
label={i18n._(t`Name`)}
value={name}
dataCy="team-detail-name"
/>
<Detail label={i18n._(t`Description`)} value={description} />
<Detail
label={i18n._(t`Organization`)}
value={
<Link to={`/organizations/${summary_fields.organization.id}`}>
{summary_fields.organization.name}
</Link>
}
/>
<Detail label={i18n._(t`Created`)} value={formatDateString(created)} />
<Detail
label={i18n._(t`Last Modified`)}
value={formatDateString(modified)}
/>
</DetailList>
<CardActionsRow>
{summary_fields.user_capabilities.edit && (
<div css="margin-top: 10px; text-align: right;">
<Button component={Link} to={`/teams/${match.params.id}/edit`}>
{i18n._(t`Edit`)}
</Button>
</div>
<Button component={Link} to={`/teams/${id}/edit`}>
{i18n._(t`Edit`)}
</Button>
)}
</CardBody>
);
}
</CardActionsRow>
</CardBody>
);
}
export default withI18n()(withRouter(TeamDetail));
export default withI18n()(TeamDetail);

View File

@ -11,7 +11,7 @@ import {
import styled from 'styled-components';
import { t } from '@lingui/macro';
import { CardBody } from '@components/Card';
import { CardBody, CardActionsRow } from '@components/Card';
import ContentError from '@components/ContentError';
import LaunchButton from '@components/LaunchButton';
import ContentLoading from '@components/ContentLoading';
@ -19,20 +19,12 @@ import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
import { JobTemplatesAPI } from '@api';
const ButtonGroup = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 20px;
& > :not(:first-child) {
margin-left: 20px;
}
`;
const MissingDetail = styled(Detail)`
dd& {
color: red;
}
`;
class JobTemplateDetail extends Component {
constructor(props) {
super(props);
@ -317,7 +309,7 @@ class JobTemplateDetail extends Component {
/>
)}
</DetailList>
<ButtonGroup>
<CardActionsRow>
{summary_fields.user_capabilities.edit && (
<Button
component={Link}
@ -340,7 +332,7 @@ class JobTemplateDetail extends Component {
)}
</LaunchButton>
)}
</ButtonGroup>
</CardActionsRow>
</CardBody>
)
);

View File

@ -19,8 +19,8 @@ describe('<JobTemplateDetail />', () => {
verbosity: 1,
summary_fields: {
user_capabilities: { edit: true },
created_by: { username: 'Joe' },
modified_by: { username: 'Joe' },
created_by: { id: 1, username: 'Joe' },
modified_by: { id: 1, username: 'Joe' },
credentials: [
{ id: 1, kind: 'ssh', name: 'Credential 1' },
{ id: 2, kind: 'awx', name: 'Credential 2' },
@ -28,6 +28,8 @@ describe('<JobTemplateDetail />', () => {
inventory: { name: 'Inventory' },
project: { name: 'Project' },
},
created: '2020-04-25T01:23:45.678901Z',
modified: '2020-04-25T01:23:45.678901Z',
};
const mockInstanceGroups = {
@ -100,12 +102,14 @@ describe('<JobTemplateDetail />', () => {
skip_tags: 'coffe,tea',
summary_fields: {
user_capabilities: { edit: false },
created_by: { username: 'Joe' },
modified_by: { username: 'Joe' },
created_by: { id: 1, username: 'Joe' },
modified_by: { id: 1, username: 'Joe' },
inventory: { name: 'Inventory' },
project: { name: 'Project' },
labels: { count: 1, results: [{ name: 'Label', id: 1 }] },
},
created: '2020-04-25T01:23:45.678901Z',
modified: '2020-04-25T01:23:45.678901Z',
};
const wrapper = mountWithContexts(
<JobTemplateDetail template={regularUser} />

View File

@ -134,7 +134,7 @@ class User extends Component {
{user && (
<Route
path="/users/:id/details"
render={() => <UserDetail match={match} user={user} />}
render={() => <UserDetail user={user} />}
/>
)}
<Route

View File

@ -1,77 +1,69 @@
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import React from 'react';
import { Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import { CardBody, CardActionsRow } from '@components/Card';
import { DetailList, Detail } from '@components/DetailList';
import { formatDateString } from '@util/dates';
class UserDetail extends Component {
render() {
const {
user: {
id,
username,
email,
first_name,
last_name,
last_login,
created,
is_superuser,
is_system_auditor,
summary_fields,
},
i18n,
} = this.props;
function UserDetail({ user, i18n }) {
const {
id,
username,
email,
first_name,
last_name,
last_login,
created,
is_superuser,
is_system_auditor,
summary_fields,
} = user;
let user_type;
if (is_superuser) {
user_type = i18n._(t`System Administrator`);
} else if (is_system_auditor) {
user_type = i18n._(t`System Auditor`);
} else {
user_type = i18n._(t`Normal User`);
}
return (
<CardBody>
<DetailList>
<Detail
label={i18n._(t`Username`)}
value={username}
dataCy="user-detail-username"
/>
<Detail label={i18n._(t`Email`)} value={email} />
<Detail label={i18n._(t`First Name`)} value={`${first_name}`} />
<Detail label={i18n._(t`Last Name`)} value={`${last_name}`} />
<Detail label={i18n._(t`User Type`)} value={`${user_type}`} />
{last_login && (
<Detail
label={i18n._(t`Last Login`)}
value={formatDateString(last_login)}
/>
)}
<Detail
label={i18n._(t`Created`)}
value={formatDateString(created)}
/>
</DetailList>
{summary_fields.user_capabilities.edit && (
<div css="margin-top: 10px; text-align: right;">
<Button
aria-label={i18n._(t`edit`)}
component={Link}
to={`/users/${id}/edit`}
>
{i18n._(t`Edit`)}
</Button>
</div>
)}
</CardBody>
);
let user_type;
if (is_superuser) {
user_type = i18n._(t`System Administrator`);
} else if (is_system_auditor) {
user_type = i18n._(t`System Auditor`);
} else {
user_type = i18n._(t`Normal User`);
}
return (
<CardBody>
<DetailList>
<Detail
label={i18n._(t`Username`)}
value={username}
dataCy="user-detail-username"
/>
<Detail label={i18n._(t`Email`)} value={email} />
<Detail label={i18n._(t`First Name`)} value={`${first_name}`} />
<Detail label={i18n._(t`Last Name`)} value={`${last_name}`} />
<Detail label={i18n._(t`User Type`)} value={`${user_type}`} />
{last_login && (
<Detail
label={i18n._(t`Last Login`)}
value={formatDateString(last_login)}
/>
)}
<Detail label={i18n._(t`Created`)} value={formatDateString(created)} />
</DetailList>
<CardActionsRow>
{summary_fields.user_capabilities.edit && (
<Button
aria-label={i18n._(t`edit`)}
component={Link}
to={`/users/${id}/edit`}
>
{i18n._(t`Edit`)}
</Button>
)}
</CardActionsRow>
</CardBody>
);
}
export default withI18n()(withRouter(UserDetail));
export default withI18n()(UserDetail);