Merge "PlanList and PlanCard updates"

This commit is contained in:
Jenkins 2017-07-27 09:42:04 +00:00 committed by Gerrit Code Review
commit 1c665a1d50
14 changed files with 268 additions and 141 deletions

View File

@ -19,12 +19,12 @@ import { Map } from 'immutable';
import React from 'react';
import ReactShallowRenderer from 'react-test-renderer/shallow';
import ListPlans from '../../../js/components/plan/ListPlans';
import PlansList from '../../../js/components/plan/PlansList';
import FileList from '../../../js/components/plan/FileList';
import { PlanFile } from '../../../js/immutableRecords/plans';
import store from '../../../js/store';
describe('ListPlans component', () => {
describe('PlansList component', () => {
let output;
beforeEach(() => {
@ -32,13 +32,13 @@ describe('ListPlans component', () => {
const intlProvider = new IntlProvider({ locale: 'en' }, {});
const { intl } = intlProvider.getChildContext();
shallowRenderer.render(
<ListPlans.WrappedComponent store={store} intl={intl} />
<PlansList.WrappedComponent store={store} intl={intl} />
);
output = shallowRenderer.getRenderOutput();
});
it('renders a table of plan names', () => {
expect(output.type.name).toEqual('ListPlans');
expect(output.type.name).toEqual('PlansList');
});
});

View File

@ -33,9 +33,9 @@ const messages = defineMessages({
id: 'NewPlan.cancel',
defaultMessage: 'Cancel'
},
createNewPlan: {
id: 'NewPlan.createNewPlan',
defaultMessage: 'Create New Plan'
importPlan: {
id: 'NewPlan.importPlan',
defaultMessage: 'Import Plan'
},
creatingPlanLoader: {
id: 'NewPlan.creatingPlanLoader',
@ -115,12 +115,13 @@ class NewPlan extends React.Component {
<span aria-hidden="true" className="pficon pficon-close" />
</Link>
<h4 className="modal-title">
<FormattedMessage {...messages.createNewPlan} />
<FormattedMessage {...messages.importPlan} />
</h4>
</div>
<Loader
loaded={!this.props.isTransitioningPlan}
size="lg"
height={100}
content={this.props.intl.formatMessage(messages.creatingPlanLoader)}
>
<ModalFormErrorList errors={this.props.planFormErrors.toJS()} />

View File

@ -27,9 +27,9 @@ const messages = defineMessages({
id: 'NoPlans.noPlansAvailableMessage',
defaultMessage: 'There are no Deployment Plans available. Please create one first.'
},
createNewPlan: {
id: 'NoPlans.createNewPlan',
defaultMessage: 'Create New Plan'
importPlan: {
id: 'NoPlans.importPlan',
defaultMessage: 'Import Plan'
}
});
@ -46,7 +46,7 @@ export default class NoPlans extends React.Component {
<Link to="/plans/manage/new" className="btn btn-lg btn-primary">
<span className="fa fa-plus" />
{' '}
<FormattedMessage {...messages.createNewPlan} />
<FormattedMessage {...messages.importPlan} />
</Link>
</div>
</div>

View File

@ -17,7 +17,7 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import ListPlans from './ListPlans';
import PlansList from './PlansList';
import DeletePlan from './DeletePlan';
import EditPlan from './EditPlan';
import ExportPlan from './ExportPlan';
@ -28,7 +28,7 @@ export default class Plans extends React.Component {
return (
<div className="row">
<div className="col-sm-12">
<Route path="/plans/manage" component={ListPlans} />
<Route path="/plans/manage" component={PlansList} />
<Switch>
<Route path="/plans/manage/new" component={NewPlan} />
<Route

View File

@ -17,6 +17,7 @@
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
@ -24,17 +25,22 @@ import { getCurrentPlanName, getPlans } from '../../selectors/plans';
import { PageHeader } from '../ui/PageHeader';
import PlansActions from '../../actions/PlansActions';
import StacksActions from '../../actions/StacksActions';
import NoPlans from './NoPlans';
import PlanCard from './cards/PlanCard';
import CreatePlanCard from './cards/CreatePlanCard';
import ImportPlanCard from './cards/ImportPlanCard';
const messages = defineMessages({
importPlan: {
id: 'PlansList.importPlan',
defaultMessage: 'Import Plan'
},
plans: {
id: 'ListPlans.plans',
id: 'PlansList.plans',
defaultMessage: 'Plans'
}
});
class ListPlans extends React.Component {
class PlansList extends React.Component {
constructor() {
super();
}
@ -66,21 +72,29 @@ class ListPlans extends React.Component {
<div>
<PageHeader>
<FormattedMessage {...messages.plans} />
</PageHeader>
<div className="panel panel-default">
<div className="cards-pf">
<div className="row row-cards-pf">
<CreatePlanCard />
{this.renderCards()}
</div>
<div className="pull-right">
<Link to="/plans/manage/new" className="btn btn-primary">
<span className="fa fa-plus" />&nbsp;
<FormattedMessage {...messages.importPlan} />
</Link>
</div>
</div>
</PageHeader>
{this.props.plans.isEmpty()
? <NoPlans />
: <div className="panel panel-default">
<div className="cards-pf">
<div className="row row-cards-pf">
<ImportPlanCard />
{this.renderCards()}
</div>
</div>
</div>}
</div>
);
}
}
ListPlans.propTypes = {
PlansList.propTypes = {
children: PropTypes.node,
currentPlanName: PropTypes.string,
fetchPlans: PropTypes.func,
@ -105,5 +119,5 @@ function mapDispatchToProps(dispatch) {
}
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(ListPlans)
connect(mapStateToProps, mapDispatchToProps)(PlansList)
);

View File

@ -14,28 +14,36 @@
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { Link } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
const messages = defineMessages({
createNewPlan: {
id: 'ListPlans.createNewPlan',
defaultMessage: 'Create New Plan'
importPlan: {
id: 'ListPlans.importPlan',
defaultMessage: 'Import Plan'
}
});
const CreatePlanCard = () => (
const ImportPlanCard = ({ history }) => (
<div className="col-xs-12 col-sm-6 col-md-4 col-lg-3">
<div className="card-pf">
<div
className="card-pf import-plan-card card-pf-view card-pf-view-select card-pf-view-single-select"
onClick={() => history.push(`/plans/manage/new`)}
id="ListPlans__importPlanLink"
>
<div className="card-pf-body">
<span className="pficon pficon-add-circle-o" />&nbsp;
<Link to="/plans/manage/new" id="ListPlans__newPlanLink">
<FormattedMessage {...messages.createNewPlan} />
</Link>
<p className="text-center">
<span className="pficon pficon-add-circle-o" />&nbsp;
<FormattedMessage {...messages.importPlan} />
</p>
</div>
</div>
</div>
);
ImportPlanCard.propTypes = {
history: PropTypes.object.isRequired
};
export default injectIntl(CreatePlanCard);
export default withRouter(injectIntl(ImportPlanCard));

View File

@ -0,0 +1,63 @@
import { defineMessages, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import DropdownKebab from '../../ui/dropdown/DropdownKebab';
import MenuItemLink from '../../ui/dropdown/MenuItemLink';
import { stackStates } from '../../../constants/StacksConstants';
const messages = defineMessages({
edit: {
id: 'PlanActions.edit',
defaultMessage: 'Edit'
},
export: {
id: 'PlanActions.export',
defaultMessage: 'Export'
},
delete: {
id: 'PlanActions.delete',
defaultMessage: 'Delete'
}
});
const PlanActions = ({ planName, stack }) => {
const renderEditAction = () => {
if (
stack &&
[
stackStates.CREATE_IN_PROGRESS,
stackStates.UPDATE_IN_PROGRESS,
stackStates.DELETE_IN_PROGRESS
].includes(stack.get('stack_status'))
) {
return null;
}
return (
<MenuItemLink to={`/plans/manage/${planName}/edit`}>
<FormattedMessage {...messages.edit} />
</MenuItemLink>
);
};
return (
<div className="pull-right">
<DropdownKebab id={`card-actions-${planName}`} pullRight>
{renderEditAction()}
<MenuItemLink to={`/plans/manage/${planName}/export`}>
<FormattedMessage {...messages.export} />
</MenuItemLink>
{!stack &&
<MenuItemLink to={`/plans/manage/${planName}/delete`}>
<FormattedMessage {...messages.delete} />
</MenuItemLink>}
</DropdownKebab>
</div>
);
};
PlanActions.propTypes = {
planName: PropTypes.string.isRequired,
stack: PropTypes.object
};
export default PlanActions;

View File

@ -14,110 +14,64 @@
* under the License.
*/
import ClassNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import {
defineMessages,
FormattedMessage,
injectIntl,
FormattedTime,
FormattedDate
FormattedDate,
FormattedTime
} from 'react-intl';
import Loader from '../../ui/Loader';
import MenuItemLink from '../../ui/dropdown/MenuItemLink';
import DropdownKebab from '../../ui/dropdown/DropdownKebab';
import PlanActions from './PlanActions';
import {
stackStates,
deploymentStatusMessages
} from '../../../constants/StacksConstants';
const messages = defineMessages({
deletingPlanName: {
id: 'ListPlans.deletingPlanName',
deleting: {
id: 'PlanCard.deletingPlanName',
defaultMessage: 'Deleting {planName}...'
},
edit: {
id: 'ListPlans.edit',
defaultMessage: 'Edit'
},
export: {
id: 'ListPlans.export',
defaultMessage: 'Export'
},
delete: {
id: 'ListPlans.delete',
defaultMessage: 'Delete'
},
notDeployed: {
id: 'ListPlans.notDeployed',
id: 'PlanCard.notDeployed',
defaultMessage: 'Not deployed'
}
});
class PlanCard extends React.Component {
getActiveIcon() {
if (this.props.plan.name === this.props.currentPlanName) {
return <span className="pficon pficon-flag" />;
}
return false;
}
renderStackIcon() {
const { stack } = this.props;
if (stack) {
switch (stack.get('stack_status')) {
case stackStates.CREATE_IN_PROGRESS:
case stackStates.UPDATE_IN_PROGRESS:
case stackStates.DELETE_IN_PROGRESS:
return <Loader inline />;
renderPlanName() {
if (this.props.plan.transition === 'deleting') {
return (
<FormattedMessage
{...messages.deletingPlanName}
values={{ planName: <strong>{this.props.plan.name}</strong> }}
/>
);
} else {
return (
<Link to={`/plans/${this.props.plan.name}`}>
{this.props.plan.name}
</Link>
);
case stackStates.CREATE_COMPLETE:
case stackStates.UPDATE_COMPLETE:
return <span className="pficon pficon-ok" />;
case stackStates.CREATE_FAILED:
case stackStates.UPDATE_FAILED:
return <span className="pficon pficon-error-circle-o" />;
}
}
}
_getIcon(stack) {
switch (stack.get('stack_status')) {
case stackStates.CREATE_IN_PROGRESS:
case stackStates.UPDATE_IN_PROGRESS:
case stackStates.DELETE_IN_PROGRESS:
return <Loader inline />;
case stackStates.CREATE_COMPLETE:
case stackStates.UPDATE_COMPLETE:
return <span className="pficon pficon-ok" />;
case stackStates.CREATE_FAILED:
case stackStates.UPDATE_FAILED:
return <span className="pficon pficon-error-circle-o" />;
}
}
_getStatus(stack) {
const { formatMessage } = this.props.intl;
return formatMessage(
getDeploymentStatus(stack) {
return this.props.intl.formatMessage(
stack
? deploymentStatusMessages[stack.get('stack_status')]
: messages.notDeployed
);
}
_renderStackInfoIcon(stack) {
const icon = stack
? this._getIcon(stack)
: <span className="pficon pficon-info" />;
const status = this._getStatus(stack);
return (
<span data-tooltip={status} className="tooltip-right">
{icon}
</span>
);
}
renderStackInfo() {
const { stack } = this.props;
let modified = null;
@ -126,7 +80,7 @@ class PlanCard extends React.Component {
const time = stack.get('updated_time') || stack.get('creation_time');
modified = (
<span>
Last modified:
<strong>Last modified:</strong>
&nbsp;
<FormattedDate value={time} />
&nbsp;
@ -136,38 +90,61 @@ class PlanCard extends React.Component {
}
return (
<p>
{modified}
{this._renderStackInfoIcon(stack)}
</p>
<ul className="list-unstyled">
<li>
<strong>Status:</strong>
{' '}
{this.renderStackIcon(stack)}
{' '}
{this.getDeploymentStatus(stack)}
</li>
<li>
{modified}
</li>
</ul>
);
}
render() {
const planName = this.props.plan.name;
const {
currentPlanName,
intl: { formatMessage },
history,
plan,
stack
} = this.props;
const cardClasses = ClassNames({
'plan-card card-pf card-pf-view card-pf-view-select card-pf-view-single-select': true,
active: plan.name === currentPlanName
});
return (
<div className="col-xs-12 col-sm-6 col-md-4 col-lg-3">
<div className="card-pf">
<div
className={cardClasses}
onClick={() => history.push(`/plans/${plan.name}`)}
>
<Loader
loaded={!plan.transition}
content={
plan.transition
? formatMessage(messages[plan.transition], {
planName: plan.name
})
: ''
}
height={62}
overlay
/>
<h2 className="card-pf-title">
{this.renderPlanName()}
&nbsp;
{this.getActiveIcon()}
<div className="pull-right">
<DropdownKebab id={`card-actions-${planName}`} pullRight>
<MenuItemLink to={`/plans/manage/${planName}/edit`}>
<FormattedMessage {...messages.edit} />
</MenuItemLink>
<MenuItemLink to={`/plans/manage/${planName}/export`}>
<FormattedMessage {...messages.export} />
</MenuItemLink>
<MenuItemLink to={`/plans/manage/${planName}/delete`}>
<FormattedMessage {...messages.delete} />
</MenuItemLink>
</DropdownKebab>
</div>
{plan.name}
<PlanActions planName={plan.name} stack={stack} />
</h2>
<div className="card-pf-body">
{/* TODO(hpokorny): fetchPlans() doesn't provide description yet */}
{plan.description &&
<div className="card-pf-body">
{plan.description}
</div>}
<div className="card-pf-footer">
{this.renderStackInfo()}
</div>
</div>
@ -178,9 +155,10 @@ class PlanCard extends React.Component {
PlanCard.propTypes = {
currentPlanName: PropTypes.string,
intl: PropTypes.object,
plan: PropTypes.object,
history: PropTypes.object.isRequired,
intl: PropTypes.object.isRequired,
plan: PropTypes.object.isRequired,
stack: PropTypes.object
};
export default injectIntl(PlanCard);
export default withRouter(injectIntl(PlanCard));

View File

@ -43,6 +43,24 @@ export default class Loader extends React.Component {
);
}
renderOverlayLoader(classes) {
return (
<div
style={{
paddingTop: `${this.props.height / 2}px`,
paddingBottom: `${this.props.height / 2}px`
}}
className="overlay-loader"
onClick={e => {
e.stopPropagation();
}}
>
<div className={classes} />
<div className="text-center">{this.props.content}</div>
</div>
);
}
renderDefaultLoader(classes) {
return (
<div
@ -75,6 +93,8 @@ export default class Loader extends React.Component {
return this.renderGlobalLoader(classes);
} else if (this.props.inline) {
return this.renderInlineLoader(classes);
} else if (this.props.overlay) {
return this.renderOverlayLoader(classes);
} else {
return this.renderDefaultLoader(classes);
}
@ -100,6 +120,7 @@ Loader.propTypes = {
inline: PropTypes.bool,
inverse: PropTypes.bool,
loaded: PropTypes.bool,
overlay: PropTypes.bool.isRequired,
size: PropTypes.oneOf(['xs', 'sm', 'lg', 'xl'])
};
Loader.defaultProps = {
@ -108,5 +129,6 @@ Loader.defaultProps = {
content: '',
global: false,
height: 10,
inline: false
inline: false,
overlay: false
};

View File

@ -21,7 +21,11 @@ import React from 'react';
const DropdownKebab = ({ children, id, pullRight }) => {
return (
<Dropdown className="dropdown-kebab-pf" id={id} pullRight={pullRight}>
<Dropdown.Toggle bsStyle="link" noCaret>
<Dropdown.Toggle
bsStyle="link"
noCaret
onClick={e => e.stopPropagation()}
>
<span className="fa fa-ellipsis-v" />
</Dropdown.Toggle>
<Dropdown.Menu>

View File

@ -23,6 +23,7 @@ import { List, Map, Record } from 'immutable';
*/
export const Plan = Record({
name: '',
description: undefined,
transition: false,
files: Map(),
isRequestingPlanDeploy: false

View File

@ -145,6 +145,7 @@
@import "ui/Grid";
@import "ui/ListView";
@import "ui/Modals";
@import "ui/Plans";
@import "ui/Tables";
@import "ui/Type";
@import "ui/Navs";
@ -155,6 +156,7 @@
@import "components/EnvironmentConfiguration";
@import "components/DeploymentDetail";
@import "components/DeploymentPlan";
@import "components/Loader";
@import "components/NodePickerInput";
@import "components/Plans";
@import "components/RoleCard";

View File

@ -0,0 +1,12 @@
.overlay-loader {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
background: rgba(255,255,255,.9);
z-index: 1;
&:hover {
cursor: auto;
}
}

22
src/less/ui/Plans.less Normal file
View File

@ -0,0 +1,22 @@
.plan-card {
.card-pf-title {
margin-bottom: 20px;
}
.card-pf-footer {
padding: 11px 20px 1px;
}
}
.import-plan-card {
border: 2px dashed @card-pf-border-color;
background: transparent;
box-shadow: none !important;
&:hover {
color: @card-pf-selected-border-color;
border-color: @card-pf-selected-border-color;
}
.card-pf-body {
margin-top: 40px;
padding-bottom: 40px;
}
}