Adds AddOrgBtn to Orgs List empty state

This commit is contained in:
Alex Corey
2019-05-17 11:10:44 -04:00
parent 5df2b1f346
commit d3cc1a8771
10 changed files with 317 additions and 300 deletions

View File

@@ -34,6 +34,7 @@ const AWXToolbar = styled.div`
const Toolbar = styled(PFToolbar)`
flex-grow: 1;
margin-left: ${props => (props.marginleft ? '0' : '20px')};
margin-right: 20px;
`;
const ToolbarGroup = styled(PFToolbarGroup)`
@@ -67,6 +68,11 @@ const AdditionalControlsWrapper = styled.div`
display: flex;
flex-grow: 1;
justify-content: flex-end;
align-items: center;
& > :not(:first-child) {
margin-left: 20px;
}
`;
class DataListToolbar extends React.Component {

View File

@@ -6,17 +6,17 @@ import {
DataListItemRow,
DataListItemCells,
DataListCell,
Text,
TextContent,
Title,
EmptyState,
EmptyStateIcon,
EmptyStateBody,
EmptyStateBody
} from '@patternfly/react-core';
import { CubesIcon } from '@patternfly/react-icons';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { withRouter, Link } from 'react-router-dom';
import styled from 'styled-components';
import Pagination from '../Pagination';
import DataListToolbar from '../DataListToolbar';
@@ -24,19 +24,26 @@ import {
parseNamespacedQueryString,
updateNamespacedQueryString,
} from '../../util/qs';
import { pluralize, getArticle, ucFirst } from '../../util/strings';
import { pluralize, ucFirst } from '../../util/strings';
import { QSConfig } from '../../types';
const detailWrapperStyle = {
display: 'grid',
gridTemplateColumns: 'minmax(70px, max-content) minmax(60px, max-content)',
};
const EmptyStateControlsWrapper = styled.div`
display: flex;
margin-top: 20px;
margin-right: 20px;
margin-bottom: 20px;
justify-content: flex-end;
const detailLabelStyle = {
fontWeight: '700',
lineHeight: '24px',
marginRight: '20px',
};
& > :not(:first-child) {
margin-left: 20px;
}
`;
const ListItemGrid = styled(TextContent)`
display: grid;
grid-template-columns: minmax(70px,max-content) repeat(auto-fit, minmax(60px,max-content));
grid-gap: 10px;
`;
class PaginatedDataList extends React.Component {
constructor (props) {
@@ -95,6 +102,7 @@ class PaginatedDataList extends React.Component {
render () {
const {
emptyStateControls,
items,
itemCount,
qsConfig,
@@ -125,72 +133,78 @@ class PaginatedDataList extends React.Component {
)}
</Fragment> // TODO: replace with proper error handling
)}
{items.length === 0 ? (
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title size="lg">
{i18n._(t`No ${ucFirst(itemNamePlural || pluralize(itemName))} Found`)}
</Title>
<EmptyStateBody>
{i18n._(t`Please add ${getArticle(itemName)} ${itemName} to populate this list`)}
</EmptyStateBody>
</EmptyState>
) : (
<Fragment>
<DataListToolbar
sortedColumnKey={orderBy}
sortOrder={sortOrder}
columns={columns}
onSearch={() => { }}
onSort={this.handleSort}
showSelectAll={showSelectAll}
isAllSelected={isAllSelected}
onSelectAll={onSelectAll}
additionalControls={additionalControls}
noLeftMargin={alignToolbarLeft}
/>
<DataList aria-label={i18n._(t`${ucFirst(pluralize(itemName))} List`)}>
{items.map(item => (renderItem ? renderItem(item) : (
<DataListItem
aria-labelledby={`items-list-item-${item.id}`}
key={item.id}
>
<DataListItemRow>
<DataListItemCells dataListCells={[
<DataListCell key="team-name">
<TextContent style={detailWrapperStyle}>
<Link to={{ pathname: item.url }}>
<Text
id={`items-list-item-${item.id}`}
style={detailLabelStyle}
>
{item.name}
</Text>
</Link>
</TextContent>
</DataListCell>
]}
/>
</DataListItemRow>
</DataListItem>
)))}
</DataList>
<Pagination
variant="bottom"
itemCount={itemCount}
page={queryParams.page || 1}
perPage={queryParams.page_size}
perPageOptions={showPageSizeOptions ? [
{ title: '5', value: 5 },
{ title: '10', value: 10 },
{ title: '20', value: 20 },
{ title: '50', value: 50 }
] : []}
onSetPage={this.handleSetPage}
onPerPageSelect={this.handleSetPageSize}
/>
</Fragment>
)}
{items.length === 0
? (
<Fragment>
<EmptyStateControlsWrapper>
{emptyStateControls}
</EmptyStateControlsWrapper>
<div css="border-bottom: 1px solid #d2d2d2" />
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title size="lg">
{i18n._(t`No ${ucFirst(itemNamePlural || pluralize(itemName))} Found `)}
</Title>
<EmptyStateBody>
{i18n._(t`Please add ${ucFirst(itemNamePlural || pluralize(itemName))} to populate this list `)}
</EmptyStateBody>
</EmptyState>
</Fragment>
)
: (
<Fragment>
<DataListToolbar
sortedColumnKey={orderBy}
sortOrder={sortOrder}
columns={columns}
onSearch={() => { }}
onSort={this.handleSort}
showSelectAll={showSelectAll}
isAllSelected={isAllSelected}
onSelectAll={onSelectAll}
additionalControls={additionalControls}
noLeftMargin={alignToolbarLeft}
/>
<DataList aria-label={i18n._(t`${ucFirst(pluralize(itemName))} List`)}>
{items.map(item => (renderItem ? renderItem(item) : (
<DataListItem
aria-labelledby={`items-list-item-${item.id}`}
key={item.id}
>
<DataListItemRow>
<DataListItemCells dataListCells={[
<DataListCell key="team-name">
<ListItemGrid>
<Link to={{ pathname: item.url }}>
<b id={`items-list-item-${item.id}`}>
{item.name}
</b>
</Link>
</ListItemGrid>
</DataListCell>
]}
/>
</DataListItemRow>
</DataListItem>
)))}
</DataList>
<Pagination
variant="bottom"
itemCount={itemCount}
page={queryParams.page || 1}
perPage={queryParams.page_size}
perPageOptions={showPageSizeOptions ? [
{ title: '5', value: 5 },
{ title: '10', value: 10 },
{ title: '20', value: 20 },
{ title: '50', value: 50 }
] : []}
onSetPage={this.handleSetPage}
onPerPageSelect={this.handleSetPageSize}
/>
</Fragment>
)}
</Fragment>
);
}

View File

@@ -1,22 +1,17 @@
import React from 'react';
import { string, func } from 'prop-types';
import { Link } from 'react-router-dom';
import { Button as PFButton } from '@patternfly/react-core';
import { Button as PFButton, Tooltip } from '@patternfly/react-core';
import { PlusIcon } from '@patternfly/react-icons';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
const Button = styled(PFButton)`
&&& { /* higher specificity order */
&& {
background-color: #5cb85c;
min-width: 0;
width: 30px;
height: 30px;
text-align: center;
padding: 0;
margin: 0;
margin-right: 20px;
padding: 5px 8px;
--pf-global--FontSize--md: 14px;
}
`;
@@ -25,19 +20,22 @@ function ToolbarAddButton ({ linkTo, onClick, i18n }) {
throw new Error('ToolbarAddButton requires either `linkTo` or `onClick` prop');
}
if (linkTo) {
// TODO: This should only be a <Link> (no <Button>) but CSS is off
return (
<Link to={linkTo}>
<Tooltip
content={i18n._(t`Add`)}
position="top"
>
<Button
component={Link}
to={linkTo}
variant="primary"
aria-label={i18n._(t`Add`)}
>
<PlusIcon />
</Button>
</Link>
</Tooltip>
);
}
return (
<Button
variant="primary"

View File

@@ -1,6 +1,6 @@
import React, { Fragment } from 'react';
import { func, bool, number, string, arrayOf, shape } from 'prop-types';
import { Button as PFButton, Tooltip } from '@patternfly/react-core';
import { Button, Tooltip } from '@patternfly/react-core';
import { TrashAltIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import { withI18n } from '@lingui/react';
@@ -8,32 +8,18 @@ import { t } from '@lingui/macro';
import AlertModal from '../AlertModal';
import { pluralize } from '../../util/strings';
const Button = styled(PFButton)`
width: 30px;
height: 30px;
display: flex;
justify-content: center;
margin-right: 20px;
border-radius: 3px;
padding: 0;
&:disabled {
cursor: not-allowed;
&:hover {
background-color: white;
> svg {
color: #d2d2d2;
}
}
}
const DeleteButton = styled(Button)`
padding: 5px 8px;
&:hover {
background-color:#d9534f;
> svg {
color: white;
}
color: white;
}
&[disabled] {
color: var(--pf-c-button--m-plain--Color);
pointer-events: initial;
cursor: not-allowed;
}
`;
@@ -118,21 +104,25 @@ class ToolbarDeleteButton extends React.Component {
const isDisabled = itemsToDelete.length === 0
|| itemsToDelete.some(cannotDelete);
// NOTE: Once PF supports tooltips on disabled elements,
// we can delete the extra <div> around the <DeleteButton> below.
// See: https://github.com/patternfly/patternfly-react/issues/1894
return (
<Fragment>
<Tooltip
content={this.renderTooltip()}
position="left"
position="top"
>
<Button
className="awx-ToolBarBtn"
variant="plain"
aria-label={i18n._(t`Delete`)}
onClick={this.handleConfirmDelete}
isDisabled={isDisabled}
>
<TrashAltIcon className="awx-ToolBarTrashCanIcon" />
</Button>
<div>
<DeleteButton
variant="plain"
aria-label={i18n._(t`Delete`)}
onClick={this.handleConfirmDelete}
isDisabled={isDisabled}
>
<TrashAltIcon />
</DeleteButton>
</div>
</Tooltip>
{ isModalOpen && (
<AlertModal

View File

@@ -113,12 +113,6 @@ class Organization extends Component {
isAdminOfThisOrg
} = this.state;
const tabsStyle = {
paddingTop: '0px',
paddingLeft: '0px',
paddingRight: '0px',
};
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin || isAuditorOfThisOrg;
const canToggleNotifications = isNotifAdmin && (
me.is_system_auditor
@@ -141,11 +135,9 @@ class Organization extends Component {
}
let cardHeader = (
loading ? ''
: (
<CardHeader
style={tabsStyle}
>
loading ? '' : (
<CardHeader style={{ padding: 0 }}>
<React.Fragment>
<div className="awx-orgTabs-container">
<RoutedTabs
match={match}
@@ -158,8 +150,10 @@ class Organization extends Component {
className="awx-orgTabs__bottom-border"
/>
</div>
</CardHeader>
));
</React.Fragment>
</CardHeader>
)
);
if (!match) {
cardHeader = null;
}

View File

@@ -4,45 +4,60 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
CardBody,
CardBody as PFCardBody,
Button,
Text,
TextContent,
TextVariants,
TextList,
TextListItem,
TextListVariants,
TextListItemVariants,
} from '@patternfly/react-core';
import styled from 'styled-components';
import { withNetwork } from '../../../../contexts/Network';
import BasicChip from '../../../../components/BasicChip/BasicChip';
const detailWrapperStyle = {
display: 'flex'
};
const CardBody = styled(PFCardBody)`
padding-top: 20px;
`;
const detailLabelStyle = {
fontWeight: '700',
lineHeight: '24px',
marginRight: '20px',
minWidth: '150px',
textAlign: 'right'
};
const DetailList = styled(TextList)`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
grid-gap: 20px;
const detailValueStyle = {
lineHeight: '24px',
wordBreak: 'break-all'
};
& > div {
display: grid;
grid-template-columns: 10em 1fr;
grid-gap: 20px;
}
`;
const DetailName = styled(TextListItem)`
&& {
grid-column: 1;
font-weight: var(--pf-global--FontWeight--bold);
text-align: right;
}
`;
const DetailValue = styled(TextListItem)`
&& {
grid-column: 2;
word-break: break-all;
}
`;
const InstanceGroupsDetail = styled.div`
grid-column: 1 / -1;
`;
const Detail = ({ label, value }) => {
let detail = null;
if (value) {
detail = (
<TextContent style={detailWrapperStyle}>
<Text component={TextVariants.h6} style={detailLabelStyle}>{ label }</Text>
<Text component={TextVariants.p} style={detailValueStyle}>{ value }</Text>
</TextContent>
);
}
return detail;
if (!value) return null;
return (
<div>
<DetailName component={TextListItemVariants.dt}>{label}</DetailName>
<DetailValue component={TextListItemVariants.dd}>{value}</DetailValue>
</div>
);
};
class OrganizationDetail extends Component {
@@ -129,7 +144,7 @@ class OrganizationDetail extends Component {
return (
<CardBody>
<div className="pf-l-grid pf-m-gutter pf-m-all-12-col-on-md pf-m-all-6-col-on-lg pf-m-all-4-col-on-xl">
<DetailList component={TextListVariants.dl}>
<Detail
label={i18n._(t`Name`)}
value={name}
@@ -151,25 +166,25 @@ class OrganizationDetail extends Component {
value={modified}
/>
{(instanceGroups && instanceGroups.length > 0) && (
<TextContent style={{ display: 'flex', gridColumn: '1 / -1' }}>
<Text
component={TextVariants.h6}
style={detailLabelStyle}
>
<InstanceGroupsDetail>
<DetailName component={TextListItemVariants.dt}>
{i18n._(t`Instance Groups`)}
</Text>
<div style={detailValueStyle}>
</DetailName>
<DetailValue component={TextListItemVariants.dd}>
{instanceGroupChips}
{overflowChip}
</div>
</TextContent>
</DetailValue>
</InstanceGroupsDetail>
)}
</div>
</DetailList>
{summary_fields.user_capabilities.edit && (
<div style={{ display: 'flex', flexDirection: 'row-reverse', marginTop: '20px' }}>
<Link to={`/organizations/${match.params.id}/edit`}>
<Button>{i18n._(t`Edit`)}</Button>
</Link>
<div css="margin-top: 10px; text-align: right;">
<Button
component={Link}
to={`/organizations/${match.params.id}/edit`}
>
{i18n._(t`Edit`)}
</Button>
</div>
)}
{error ? 'error!' : ''}

View File

@@ -186,6 +186,10 @@ class OrganizationsList extends Component {
onSelect={() => this.handleSelect(o)}
/>
)}
emptyStateControls={
canAdd ? <ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
: null
}
/>
)}
{ isLoading ? <div>loading...</div> : '' }