Plans routing refactor

* Plans are now top level navigation item
* No plan needs to be active by default - fixes the problem when no
  plans exist
* Plan is activated by visiting /plans/<planName> url
* On application initialization (page refresh) currentPlanName in
  in application state is populated from local storage
* Plans selectors have been updated to find plan and plan name based
  on comparing list of a plans to currentPlanName - this ensures that
  active plan always exist
* When a plan is active, user can use breadcrumb navigation to get to
  plan management page and activate/manage plans
* detectPlan action has been removed as it is no longer needed
* Routing has been updated to match new structure

Change-Id: Ia5d4b3444c27edafa5b9209314dead1625dde5ef
This commit is contained in:
Jiri Tomasek 2017-06-20 12:51:02 +02:00 committed by Ana Krivokapic
parent 938119cc84
commit a2d103b83c
48 changed files with 719 additions and 779 deletions

View File

@ -0,0 +1,6 @@
---
features:
- |
'Plans' are now top level navigation item instead of deployment plan. No
deployment plan needs to be active by default. Activating deployment plan
is done by visiting plan via url.

View File

@ -17,6 +17,7 @@
import when from 'when';
import { Map } from 'immutable';
import { InitialPlanState, Plan } from '../../js/immutableRecords/plans';
import MistralApiService from '../../js/services/MistralApiService';
import mockHistory from '../mocks/history';
import { mockGetIntl } from './utils';
@ -112,6 +113,14 @@ describe('nodesRegistrationFinished', () => {
() => {},
() => {
return {
plans: new InitialPlanState({
plansLoaded: true,
all: Map({
testplan: new Plan({
name: 'testplan'
})
})
}),
currentPlan: new CurrentPlanState({
currentPlanName: 'testplan'
})
@ -163,6 +172,14 @@ describe('nodesRegistrationFinished', () => {
() => {},
() => {
return {
plans: new InitialPlanState({
plansLoaded: true,
all: Map({
testplan: new Plan({
name: 'testplan'
})
})
}),
currentPlan: new CurrentPlanState({
currentPlanName: 'testplan'
})

View File

@ -1,76 +0,0 @@
/**
* Copyright 2017 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { List, Map } from 'immutable';
import { InitialPlanState } from '../../../js/immutableRecords/plans';
import { CurrentPlanState } from '../../../js/immutableRecords/currentPlan';
import {
ParametersDefaultState
} from '../../../js/immutableRecords/parameters';
import { RolesState } from '../../../js/immutableRecords/roles';
import { StacksState } from '../../../js/immutableRecords/stacks';
import {
EnvironmentConfigurationState
} from '../../../js/immutableRecords/environmentConfiguration';
import {
mapStateToProps
} from '../../../js/components/deployment_plan/DeploymentPlan.js';
describe('DeploymentPlan mapStateToProps', () => {
describe('hasPlans flag', () => {
it('returns ``hasPlans`` as `false`', () => {
let props = mapStateToProps({
currentPlan: new CurrentPlanState(),
parameters: new ParametersDefaultState(),
plans: new InitialPlanState({ all: List() }),
stacks: new StacksState(),
roles: new RolesState({
loaded: false,
isFetching: false,
roles: Map()
}),
nodes: Map({
isFetching: false,
all: Map()
}),
environmentConfiguration: new EnvironmentConfigurationState(),
validations: Map()
});
expect(props.hasPlans).toBe(false);
});
it('returns ``hasPlans`` as `true`', () => {
let props = mapStateToProps({
currentPlan: new CurrentPlanState(),
parameters: new ParametersDefaultState(),
plans: new InitialPlanState({ all: List(['foo', 'bar']) }),
stacks: new StacksState(),
roles: new RolesState({
loaded: false,
isFetching: false,
roles: Map()
}),
nodes: Map({
isFetching: false,
all: Map()
}),
environmentConfiguration: new EnvironmentConfigurationState(),
validations: Map()
});
expect(props.hasPlans).toBe(true);
});
});
});

View File

@ -26,10 +26,6 @@ describe('plansReducer state', () => {
state = currentPlanReducer(undefined, { type: 'undefined-action' });
});
it('`conflict` is undefined', () => {
expect(state.get('conflict')).not.toBeDefined();
});
it('`currentPlanName` is undefined', () => {
expect(state.get('currentPlanName')).not.toBeDefined();
});

View File

@ -40,7 +40,6 @@ describe('plans selectors', () => {
})
}),
currentPlan: new CurrentPlanState({
conflict: undefined,
currentPlanName: 'plan1'
})
};

View File

@ -22,6 +22,7 @@ import {
getOvercloudInfo
} from '../../js/selectors/stacks';
import { CurrentPlanState } from '../../js/immutableRecords/currentPlan';
import { InitialPlanState, Plan } from '../../js/immutableRecords/plans';
import { Stack, StacksState } from '../../js/immutableRecords/stacks';
describe('stacks selectors', () => {
@ -41,6 +42,14 @@ describe('stacks selectors', () => {
})
}),
currentStackEnvironment: Map(),
plans: new InitialPlanState({
plansLoaded: true,
all: Map({
overcloud: new Plan({
name: 'overcloud'
})
})
}),
currentPlan: new CurrentPlanState({
currentPlanName: 'overcloud'
})
@ -118,6 +127,14 @@ describe('stacks selectors', () => {
})
})
}),
plans: new InitialPlanState({
plansLoaded: true,
all: Map({
overcloud: new Plan({
name: 'overcloud'
})
})
}),
currentPlan: new CurrentPlanState({
currentPlanName: 'overcloud'
})
@ -145,6 +162,14 @@ describe('stacks selectors', () => {
})
})
}),
plans: new InitialPlanState({
plansLoaded: true,
all: Map({
overcloud: new Plan({
name: 'overcloud'
})
})
}),
currentPlan: new CurrentPlanState({
currentPlanName: 'overcloud'
})
@ -166,6 +191,14 @@ describe('stacks selectors', () => {
})
})
}),
plans: new InitialPlanState({
plansLoaded: true,
all: Map({
overcloud: new Plan({
name: 'overcloud'
})
})
}),
currentPlan: new CurrentPlanState({
currentplanname: 'overcloud'
})
@ -183,6 +216,14 @@ describe('stacks selectors', () => {
})
})
}),
plans: new InitialPlanState({
plansLoaded: true,
all: Map({
overcloud: new Plan({
name: 'overcloud'
})
})
}),
currentPlan: new CurrentPlanState({
currentplanname: 'overcloud'
})

View File

@ -23,6 +23,7 @@ import {
FiltersInitialState
} from '../../js/immutableRecords/filters';
import { CurrentPlanState } from '../../js/immutableRecords/currentPlan';
import { InitialPlanState, Plan } from '../../js/immutableRecords/plans';
import {
WorkflowExecution
} from '../../js/immutableRecords/workflowExecutions';
@ -32,8 +33,15 @@ describe(' validations selectors', () => {
let state;
beforeEach(() => {
state = {
plans: new InitialPlanState({
plansLoaded: true,
all: Map({
overcloud: new Plan({
name: 'overcloud'
})
})
}),
currentPlan: new CurrentPlanState({
conflict: undefined,
currentPlanName: 'overcloud'
}),
filters: FiltersInitialState(),

View File

@ -14,83 +14,31 @@
* under the License.
*/
import { defineMessages } from 'react-intl';
import NotificationActions from '../actions/NotificationActions';
import PlansConstants from '../constants/PlansConstants';
import ValidationsActions from '../actions/ValidationsActions';
const messages = defineMessages({
planActivatedNotificationTitle: {
id: 'CurrentPlanActions.planActivatedNotificationTitle',
defaultMessage: 'Plan Activated'
},
planActivatedNotificationMessage: {
id: 'CurrentPlanActions.planActivatedNotificationMessage',
defaultMessage: 'The plan {planName} was activated.'
}
});
import { getPlans, getCurrentPlanName } from '../selectors/plans';
export default {
detectPlan() {
choosePlan(newPlanName) {
return (dispatch, getState) => {
let state = getState();
let plans = state.plans.all.map(plan => plan.get('name'));
let conflict;
let currentPlanName = state.currentPlan.get('currentPlanName');
let previousPlan = currentPlanName || getStoredPlan();
// No plans present.
if (plans.size < 1) {
if (!previousPlan) {
currentPlanName = undefined;
const currentPlanName = getCurrentPlanName(getState());
const plans = getPlans(getState());
if (plans.get(newPlanName)) {
if (newPlanName !== currentPlanName) {
storePlan(newPlanName);
dispatch(this.planChosen(newPlanName));
dispatch(
ValidationsActions.runValidationGroups(
['prep', 'pre-deployment'],
newPlanName
)
);
}
} else if (!previousPlan) {
// Plans present.
// No previously chosen plan.
currentPlanName = plans.first();
} else if (!plans.includes(previousPlan)) {
// Previously chosen plan doesn't exist any more.
conflict = previousPlan;
currentPlanName = plans.first();
} else if (!currentPlanName && previousPlan) {
// No plan in state, but in localStorage
currentPlanName = previousPlan;
} else {
storePlan();
dispatch(this.planChosen());
}
storePlan(currentPlanName);
dispatch(this.planDetected(currentPlanName, conflict));
};
},
planDetected(currentPlanName, conflict) {
return {
type: PlansConstants.PLAN_DETECTED,
payload: {
currentPlanName: currentPlanName,
conflict: conflict
}
};
},
choosePlan(planName) {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(
NotificationActions.notify({
title: formatMessage(messages.planActivatedNotificationTitle),
message: formatMessage(messages.planActivatedNotificationMessage, {
planName: planName
}),
type: 'success'
})
);
storePlan(planName);
dispatch(this.planChosen(planName));
dispatch(
ValidationsActions.runValidationGroups(
['prep', 'pre-deployment'],
planName
)
);
};
},
@ -104,17 +52,10 @@ export default {
function storePlan(name) {
if (window && window.localStorage) {
if (!name) {
window.localStorage.removeItem('currentPlanName');
} else {
if (name) {
window.localStorage.setItem('currentPlanName', name);
} else {
window.localStorage.removeItem('currentPlanName');
}
}
}
function getStoredPlan() {
if (window && window.localStorage) {
return window.localStorage.getItem('currentPlanName');
}
return null;
}

View File

@ -19,7 +19,6 @@ import { fromJS } from 'immutable';
import { normalize, arrayOf } from 'normalizr';
import when from 'when';
import CurrentPlanActions from '../actions/CurrentPlanActions';
import logger from '../services/logger';
import MistralApiService from '../services/MistralApiService';
import MistralApiErrorHandler from '../services/MistralApiErrorHandler';
@ -88,7 +87,6 @@ export default {
.then(response => {
let plans = JSON.parse(response.output).result || [];
dispatch(this.receivePlans(plans));
dispatch(CurrentPlanActions.detectPlan());
})
.catch(error => {
logger.error(
@ -172,7 +170,7 @@ export default {
this._uploadFilesToContainer(planName, fromJS(planFiles), dispatch)
.then(() => {
dispatch(this.updatePlanSuccess(planName));
history.push('/plans');
history.push('/plans/manage');
dispatch(
NotificationActions.notify({
title: formatMessage(messages.planUpdatedNotificationTitle),
@ -201,7 +199,7 @@ export default {
SwiftApiService.uploadTarball(planName, file)
.then(response => {
dispatch(this.updatePlanSuccess(planName));
history.push('/plans');
history.push('/plans/manage');
dispatch(
NotificationActions.notify({
title: formatMessage(messages.planUpdatedNotificationTitle),
@ -350,7 +348,7 @@ export default {
})
);
dispatch(this.fetchPlans());
history.push('/plans');
history.push('/plans/manage');
} else {
dispatch(
this.createPlanFailed([{ title: 'Error', message: payload.message }])
@ -439,7 +437,7 @@ export default {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.deletePlanPending(planName));
history.push('/plans');
history.push('/plans/manage');
MistralApiService.runAction(MistralConstants.PLAN_DELETE, {
container: planName
})
@ -454,7 +452,6 @@ export default {
type: 'success'
})
);
dispatch(CurrentPlanActions.detectPlan());
})
.catch(error => {
logger.error(

View File

@ -18,6 +18,7 @@ import { defineMessages } from 'react-intl';
import { normalize, arrayOf } from 'normalizr';
import { Map } from 'immutable';
import { getCurrentPlanName } from '../selectors/plans';
import RegisterNodesConstants from '../constants/RegisterNodesConstants';
import MistralApiErrorHandler from '../services/MistralApiErrorHandler';
import MistralApiService from '../services/MistralApiService';
@ -137,7 +138,7 @@ export default {
dispatch(
ValidationsActions.runValidationGroups(
['pre-introspection'],
getState().currentPlan.currentPlanName
getCurrentPlanName(getState())
)
);

View File

@ -22,6 +22,7 @@ import React from 'react';
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
import DeploymentPlan from './deployment_plan/DeploymentPlan';
import { getCurrentPlanName } from '../selectors/plans';
import Loader from './ui/Loader';
import LoginActions from '../actions/LoginActions';
import NavBar from './NavBar';
@ -47,17 +48,10 @@ class AuthenticatedContent extends React.Component {
}
render() {
const {
currentPlanName,
intl,
logoutUser,
noPlans,
plansLoaded,
user
} = this.props;
const { currentPlanName, intl, logoutUser, plansLoaded, user } = this.props;
return (
<Loader
loaded={plansLoaded && (!!currentPlanName || noPlans)}
loaded={plansLoaded}
content={intl.formatMessage(messages.loadingDeployments)}
global
>
@ -69,9 +63,11 @@ class AuthenticatedContent extends React.Component {
<div className="col-sm-12 col-lg-9">
<Switch>
<Route path="/nodes" component={Nodes} />
<Route path="/deployment-plan" component={DeploymentPlan} />
<Route path="/plans" component={Plans} />
<Redirect from="/" to="/deployment-plan" />
<Route path="/plans/manage" component={Plans} />
<Route path="/plans/:planName" component={DeploymentPlan} />
{currentPlanName
? <Redirect from="/" to={`/plans/${currentPlanName}`} />
: <Redirect from="/" to="/plans/manage" />}
</Switch>
</div>
<ValidationsList />
@ -90,7 +86,6 @@ AuthenticatedContent.propTypes = {
initializeZaqarConnection: PropTypes.func.isRequired,
intl: PropTypes.object,
logoutUser: PropTypes.func.isRequired,
noPlans: PropTypes.bool,
plansLoaded: PropTypes.bool,
user: ImmutablePropTypes.map
};
@ -105,8 +100,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
});
const mapStateToProps = state => ({
currentPlanName: state.currentPlan.currentPlanName,
noPlans: state.plans.get('all').isEmpty(),
currentPlanName: getCurrentPlanName(state),
plansLoaded: state.plans.get('plansLoaded'),
user: state.login.getIn(['token', 'user'])
});

View File

@ -35,9 +35,9 @@ const messages = defineMessages({
id: 'NavBar.logoutLink',
defaultMessage: 'Logout'
},
deploymentPlanTab: {
id: 'NavBar.deploymentPlanTab',
defaultMessage: 'Deployment Plan'
plansTab: {
id: 'NavBar.plansTab',
defaultMessage: 'Plans'
},
nodesTab: {
id: 'Navbar.nodesTab',
@ -108,8 +108,8 @@ export default class NavBar extends React.Component {
</li>
</ul>
<ul className="nav navbar-nav navbar-primary">
<NavTab to="/deployment-plan" id="NavBar__deploymentPlanTab">
<FormattedMessage {...messages.deploymentPlanTab} />
<NavTab to="/plans" id="NavBar__PlansTab">
<FormattedMessage {...messages.plansTab} />
</NavTab>
<NavTab to="/nodes" id="NavBar__nodesTab">
<FormattedMessage {...messages.nodesTab} />

View File

@ -28,7 +28,7 @@ import DeploymentConfirmation from './DeploymentConfirmation';
import DeploymentProgress from './DeploymentProgress';
import DeploymentSuccess from './DeploymentSuccess';
import DeploymentFailure from './DeploymentFailure';
import { getCurrentPlan } from '../../selectors/plans';
import { getCurrentPlan, getCurrentPlanName } from '../../selectors/plans';
import {
getCurrentStack,
getCurrentStackDeploymentProgress
@ -69,6 +69,7 @@ class DeploymentDetail extends React.Component {
const {
allPreDeploymentValidationsSuccessful,
currentPlan,
currentPlanName,
currentStack,
currentStackDeploymentProgress,
currentStackResources,
@ -124,25 +125,30 @@ class DeploymentDetail extends React.Component {
stack={currentStack}
stackResources={currentStackResources}
stackResourcesLoaded={currentStackResourcesLoaded}
planName={currentPlan.name}
planName={currentPlanName}
/>
);
}
}
render() {
const { currentPlanName } = this.props;
return (
<div>
<ModalPanelBackdrop />
<ModalPanel>
<ModalPanelHeader>
<Link to="/deployment-plan" type="button" className="close">
<Link
to={`/plans/${currentPlanName}`}
type="button"
className="close"
>
<span aria-hidden="true" className="pficon pficon-close" />
</Link>
<h2 className="modal-title">
<FormattedMessage
{...messages.modalTitle}
values={{ planName: this.props.currentPlan.name }}
values={{ planName: currentPlanName }}
/>
</h2>
</ModalPanelHeader>
@ -151,7 +157,7 @@ class DeploymentDetail extends React.Component {
</ModalPanelBody>
<ModalPanelFooter>
<Link
to="/deployment-plan"
to={`/plans/${currentPlanName}`}
type="button"
className="btn btn-default"
>
@ -167,6 +173,7 @@ class DeploymentDetail extends React.Component {
DeploymentDetail.propTypes = {
allPreDeploymentValidationsSuccessful: PropTypes.bool.isRequired,
currentPlan: ImmutablePropTypes.record.isRequired,
currentPlanName: PropTypes.string.isRequired,
currentStack: ImmutablePropTypes.record,
currentStackDeploymentProgress: PropTypes.number.isRequired,
currentStackResources: ImmutablePropTypes.map,
@ -185,6 +192,7 @@ const mapStateToProps = state => {
state
),
currentPlan: getCurrentPlan(state),
currentPlanName: getCurrentPlanName(state),
currentStack: getCurrentStack(state),
currentStackDeploymentProgress: getCurrentStackDeploymentProgress(state),
currentStackResources: state.stacks.resources,

View File

@ -33,7 +33,7 @@ const ConfigurePlanStep = props => {
<div>
<DeploymentConfigurationSummary {...props} />
&nbsp;
<Link to="/deployment-plan/configuration">
<Link to={`/plans/${props.planName}/configuration`}>
<FormattedMessage {...messages.editConfigurationLink} />
</Link>
</div>

View File

@ -0,0 +1,385 @@
/**
* Copyright 2017 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
import { Link, Route, withRouter } from 'react-router-dom';
import DeploymentConfiguration from './DeploymentConfiguration';
import DeploymentDetail from '../deployment/DeploymentDetail';
import { getAllPlansButCurrent } from '../../selectors/plans';
import {
getCurrentStack,
getCurrentStackDeploymentProgress,
getCurrentStackDeploymentInProgress,
getOvercloudInfo
} from '../../selectors/stacks';
import {
getAvailableNodes,
getAvailableNodesCountsByRole,
getNodeCountParametersByRole,
getTotalAssignedNodesCount
} from '../../selectors/nodesAssignment';
import {
getEnvironmentConfigurationSummary
} from '../../selectors/environmentConfiguration';
import { getCurrentPlan } from '../../selectors/plans';
import { getRoles } from '../../selectors/roles';
import ConfigurePlanStep from './ConfigurePlanStep';
import CurrentPlanActions from '../../actions/CurrentPlanActions';
import { DeploymentPlanStep } from './DeploymentPlanStep';
import DeployStep from './DeployStep';
import EnvironmentConfigurationActions
from '../../actions/EnvironmentConfigurationActions';
import HardwareStep from './HardwareStep';
import NodesActions from '../../actions/NodesActions';
import NotificationActions from '../../actions/NotificationActions';
import ParametersActions from '../../actions/ParametersActions';
import PlansActions from '../../actions/PlansActions';
import RoleDetail from '../roles/RoleDetail';
import RolesStep from './RolesStep';
import RolesActions from '../../actions/RolesActions';
import StacksActions from '../../actions/StacksActions';
import stackStates from '../../constants/StacksConstants';
import ValidationsActions from '../../actions/ValidationsActions';
const messages = defineMessages({
backToAllPlans: {
id: 'CurrentPlan.backToAllPlans',
defaultMessage: 'All Plans'
},
hardwareStepHeader: {
id: 'CurrentPlan.hardwareStepHeader',
defaultMessage: 'Prepare Hardware'
},
configureRolesStepHeader: {
id: 'CurrentPlan.configureRolesStepHeader',
defaultMessage: 'Configure Roles and Assign Nodes'
},
deploymentConfigurationStepHeader: {
id: 'CurrentPlan.deploymentConfigurationStepHeader',
defaultMessage: 'Specify Deployment Configuration'
},
deployStepHeader: {
id: 'CurrentPlan.deployStepHeader',
defaultMessage: 'Deploy'
},
hardwareStepTooltip: {
id: 'CurrentPlan.hardwareStepTooltip',
defaultMessage: 'This step registers and introspects your nodes. Registration involves ' +
'defining the power management details of each node so that you so that the director can ' +
'control them during the introspection and provisioning stages. After registration, you ' +
'introspect the nodes, which identifies the hardware each node uses and builds a profile of ' +
'each node. After registration and introspection, you can assign these nodes into specific ' +
'roles in your overcloud.'
},
configurePlanStepTooltip: {
id: 'CurrentPlan.configurePlanStepTooltip',
defaultMessage: "This step allows you edit specific settings for the overcloud's network, " +
'storage, and other certified plugins. Use this step to define your network isolation ' +
'configuration and your backend storage settings.'
},
configureRolesStepTooltip: {
id: 'CurrentPlan.configureRolesStepTooltip',
defaultMessage: 'This step assigns and removes nodes from roles in your overcloud. On each ' +
"role's selection dialog, you can tag available nodes into the role or untag nodes already " +
'assigned to the role. Click "Assign Nodes" for a particular role to open the selection ' +
'dialog and start assigning nodes. ' +
'You can also customize role-specific settings in this step. For example, you can compose ' +
'services on each role and customize specific parameters related to each role. Click the ' +
'pencil icon in the top-right corner of each role to see these role-specific settings'
},
deployStepTooltip: {
id: 'CurrentPlan.deploymentStepTooltip',
defaultMessage: 'This step performs the deployment of the overcloud. Once the deployment ' +
'begins, the director tracks the progress and provides a report of each completed, running, ' +
'or failed step. When the deployment completes, the director displays the current overcloud ' +
'status and login details, which you use to interact with your overcloud. Click "Deploy" to ' +
'start the deployment.'
}
});
class CurrentPlan extends React.Component {
componentDidMount() {
this.props.fetchStacks();
this.fetchParameters();
}
componentWillReceiveProps(nextProps) {
if (!nextProps.stacksLoaded) {
this.props.fetchStacks();
}
if (nextProps.currentPlan !== this.props.currentPlan) {
this.fetchParameters();
}
this.postDeploymentValidationsCheck(nextProps.currentStack);
this.pollCurrentStack(nextProps.currentStack);
}
componentWillUnmount() {
clearTimeout(this.stackProgressTimeout);
}
fetchParameters() {
!this.props.isFetchingParameters &&
this.props.fetchParameters(this.props.currentPlan.name);
}
pollCurrentStack(currentStack) {
if (currentStack) {
if (currentStack.stack_status.match(/PROGRESS/)) {
clearTimeout(this.stackProgressTimeout);
this.stackProgressTimeout = setTimeout(() => {
this.props.fetchStacks();
this.props.fetchStackResources(currentStack);
}, 20000);
}
}
}
postDeploymentValidationsCheck(nextStack) {
const { currentStack, currentPlan } = this.props;
const progressStates = [
stackStates.UPDATE_IN_PROGRESS,
stackStates.CREATE_IN_PROGRESS
];
const successStates = [
stackStates.UPDATE_COMPLETE,
stackStates.CREATE_COMPLETE
];
if (
currentStack &&
nextStack &&
progressStates.includes(currentStack.stack_status) &&
successStates.includes(nextStack.stack_status)
) {
this.props.runPostDeploymentValidations(currentPlan.name);
}
}
render() {
const { intl: { formatMessage }, currentPlan } = this.props;
const currentPlanName = currentPlan.name;
return (
<div className="row">
<div className="col-sm-12">
<ol className="breadcrumb">
<li>
<Link to="/plans/manage">
<FormattedMessage {...messages.backToAllPlans} />
</Link>
</li>
<li className="active">
{currentPlanName}
</li>
</ol>
<div className="page-header page-header-bleed-right">
<h1>{currentPlanName}</h1>
</div>
<ol className="deployment-step-list">
<DeploymentPlanStep
title={formatMessage(messages.hardwareStepHeader)}
disabled={this.props.currentStackDeploymentInProgress}
tooltip={formatMessage(messages.hardwareStepTooltip)}
>
<HardwareStep />
</DeploymentPlanStep>
<DeploymentPlanStep
title={formatMessage(messages.deploymentConfigurationStepHeader)}
disabled={this.props.currentStackDeploymentInProgress}
tooltip={formatMessage(messages.configurePlanStepTooltip)}
>
<ConfigurePlanStep
fetchEnvironmentConfiguration={
this.props.fetchEnvironmentConfiguration
}
summary={this.props.environmentConfigurationSummary}
planName={currentPlanName}
isFetching={this.props.isFetchingEnvironmentConfiguration}
loaded={this.props.environmentConfigurationLoaded}
/>
</DeploymentPlanStep>
<DeploymentPlanStep
title={formatMessage(messages.configureRolesStepHeader)}
disabled={this.props.currentStackDeploymentInProgress}
tooltip={formatMessage(messages.configureRolesStepTooltip)}
>
<RolesStep
availableNodesCount={this.props.availableNodes.size}
availableNodesCountsByRole={
this.props.availableNodesCountsByRole
}
currentPlanName={currentPlanName}
nodeCountParametersByRole={this.props.nodeCountParametersByRole}
fetchNodes={this.props.fetchNodes}
fetchRoles={this.props.fetchRoles.bind(this, currentPlanName)}
isFetchingNodes={this.props.isFetchingNodes}
isFetchingRoles={this.props.isFetchingRoles}
isFetchingParameters={this.props.isFetchingParameters}
roles={this.props.roles}
rolesLoaded={this.props.rolesLoaded}
totalAssignedNodesCount={this.props.totalAssignedNodesCount}
/>
</DeploymentPlanStep>
<DeploymentPlanStep
title={formatMessage(messages.deployStepHeader)}
tooltip={formatMessage(messages.deployStepTooltip)}
>
<DeployStep
currentPlan={currentPlan}
currentStack={this.props.currentStack}
currentStackResources={this.props.currentStackResources}
currentStackDeploymentProgress={
this.props.currentStackDeploymentProgress
}
deleteStack={this.props.deleteStack}
deployPlan={this.props.deployPlan}
fetchStackEnvironment={this.props.fetchStackEnvironment}
fetchStackResource={this.props.fetchStackResource}
overcloudInfo={this.props.overcloudInfo}
isRequestingStackDelete={this.props.isRequestingStackDelete}
stacksLoaded={this.props.stacksLoaded}
/>
</DeploymentPlanStep>
</ol>
</div>
<Route
path="/plans/:planName/configuration"
component={DeploymentConfiguration}
/>
<Route
path="/plans/:planName/roles/:roleIdentifier"
component={RoleDetail}
/>
<Route
path="/plans/:planName/deployment-detail"
component={DeploymentDetail}
/>
</div>
);
}
}
CurrentPlan.propTypes = {
availableNodes: ImmutablePropTypes.map,
availableNodesCountsByRole: ImmutablePropTypes.map.isRequired,
choosePlan: PropTypes.func,
currentPlan: ImmutablePropTypes.record,
currentStack: ImmutablePropTypes.record,
currentStackDeploymentInProgress: PropTypes.bool,
currentStackDeploymentProgress: PropTypes.number.isRequired,
currentStackResources: ImmutablePropTypes.map,
deleteStack: PropTypes.func,
deployPlan: PropTypes.func,
environmentConfigurationLoaded: PropTypes.bool,
environmentConfigurationSummary: PropTypes.string,
fetchEnvironmentConfiguration: PropTypes.func,
fetchNodes: PropTypes.func,
fetchParameters: PropTypes.func,
fetchRoles: PropTypes.func,
fetchStackEnvironment: PropTypes.func,
fetchStackResource: PropTypes.func,
fetchStackResources: PropTypes.func.isRequired,
fetchStacks: PropTypes.func,
inactivePlans: ImmutablePropTypes.map,
intl: PropTypes.object,
isFetchingEnvironmentConfiguration: PropTypes.bool,
isFetchingNodes: PropTypes.bool,
isFetchingParameters: PropTypes.bool,
isFetchingRoles: PropTypes.bool,
isRequestingStackDelete: PropTypes.bool,
nodeCountParametersByRole: ImmutablePropTypes.map,
notify: PropTypes.func,
overcloudInfo: ImmutablePropTypes.map.isRequired,
roles: ImmutablePropTypes.map,
rolesLoaded: PropTypes.bool,
route: PropTypes.object,
runPostDeploymentValidations: PropTypes.func.isRequired,
stacksLoaded: PropTypes.bool.isRequired,
totalAssignedNodesCount: PropTypes.number.isRequired
};
export function mapStateToProps(state, props) {
return {
availableNodesCountsByRole: getAvailableNodesCountsByRole(state),
nodeCountParametersByRole: getNodeCountParametersByRole(state),
availableNodes: getAvailableNodes(state),
currentPlan: getCurrentPlan(state),
currentStack: getCurrentStack(state),
currentStackResources: state.stacks.resources,
currentStackDeploymentInProgress: getCurrentStackDeploymentInProgress(
state
),
currentStackDeploymentProgress: getCurrentStackDeploymentProgress(state),
environmentConfigurationLoaded: state.environmentConfiguration.loaded,
environmentConfigurationSummary: getEnvironmentConfigurationSummary(state),
isFetchingEnvironmentConfiguration: state.environmentConfiguration
.isFetching,
isFetchingNodes: state.nodes.get('isFetching'),
isFetchingParameters: state.parameters.isFetching,
isFetchingRoles: state.roles.get('isFetching'),
overcloudInfo: getOvercloudInfo(state),
isRequestingStackDelete: state.stacks.get('isRequestingStackDelete'),
inactivePlans: getAllPlansButCurrent(state),
roles: getRoles(state),
rolesLoaded: state.roles.get('loaded'),
stacksLoaded: state.stacks.get('isLoaded'),
totalAssignedNodesCount: getTotalAssignedNodesCount(state)
};
}
function mapDispatchToProps(dispatch) {
return {
choosePlan: planName => dispatch(CurrentPlanActions.choosePlan(planName)),
deleteStack: (stackName, stackId) => {
dispatch(StacksActions.deleteStack(stackName, stackId));
},
deployPlan: planName => dispatch(PlansActions.deployPlan(planName)),
fetchStackEnvironment: stack =>
dispatch(StacksActions.fetchEnvironment(stack)),
fetchEnvironmentConfiguration: (planName, parentPath) => {
dispatch(
EnvironmentConfigurationActions.fetchEnvironmentConfiguration(
planName,
parentPath
)
);
},
fetchNodes: () => dispatch(NodesActions.fetchNodes()),
fetchParameters: planName =>
dispatch(ParametersActions.fetchParameters(planName)),
fetchRoles: planName => dispatch(RolesActions.fetchRoles(planName)),
fetchStackResources: stack =>
dispatch(StacksActions.fetchResources(stack.stack_name, stack.id)),
fetchStackResource: (stack, resourceName) =>
dispatch(StacksActions.fetchResource(stack, resourceName)),
fetchStacks: () => dispatch(StacksActions.fetchStacks()),
notify: notification => dispatch(NotificationActions.notify(notification)),
runPostDeploymentValidations: planName => {
dispatch(
ValidationsActions.runValidationGroups(['post-deployment'], planName)
);
}
};
}
export default injectIntl(
withRouter(connect(mapStateToProps, mapDispatchToProps)(CurrentPlan))
);

View File

@ -60,7 +60,7 @@ export const DeployStep = ({
<Link
className="link btn btn-primary btn-lg"
disabled={currentPlan.isRequestingPlanDeploy}
to="/deployment-plan/deployment-detail"
to={`/plans/${currentPlan.name}/deployment-detail`}
>
<Loader
loaded={!currentPlan.isRequestingPlanDeploy}
@ -78,6 +78,7 @@ export const DeployStep = ({
} else if (currentStack.stack_status.match(/PROGRESS/)) {
return (
<DeploymentProgress
currentPlanName={currentPlan.name}
stack={currentStack}
isRequestingStackDelete={isRequestingStackDelete}
deleteStack={deleteStack}
@ -99,6 +100,7 @@ export const DeployStep = ({
} else {
return (
<DeploymentFailure
currentPlanName={currentPlan.name}
deleteStack={deleteStack}
isRequestingStackDelete={isRequestingStackDelete}
stack={currentStack}

View File

@ -43,11 +43,15 @@ const messages = defineMessages({
class DeploymentConfiguration extends React.Component {
render() {
const { location } = this.props;
const { location, match } = this.props;
return (
<Modal dialogClasses="modal-xl">
<div className="modal-header">
<Link to="/deployment-plan" type="button" className="close">
<Link
to={`/plans/${match.params.planName}`}
type="button"
className="close"
>
<span aria-hidden="true" className="pficon pficon-close" />
</Link>
<h4 className="modal-title">
@ -56,34 +60,26 @@ class DeploymentConfiguration extends React.Component {
</div>
<ul className="nav nav-tabs">
<NavTab
location={location}
to="/deployment-plan/configuration/environment"
>
<NavTab to={`${match.url}/environment`}>
<FormattedMessage {...messages.overallSettings} />
</NavTab>
<NavTab
location={location}
to="/deployment-plan/configuration/parameters"
>
<NavTab to={`${match.url}/parameters`}>
<FormattedMessage {...messages.parameters} />
</NavTab>
</ul>
<Switch location={location}>
<Route
location={location}
path="/deployment-plan/configuration/environment"
path="/plans/:planName/configuration/environment"
component={EnvironmentConfiguration}
/>
<Route
location={location}
path="/deployment-plan/configuration/parameters"
path="/plans/:planName/configuration/parameters"
component={Parameters}
/>
<Redirect
from="/deployment-plan/configuration"
to="/deployment-plan/configuration/environment"
from="/plans/:planName/configuration"
to={`${match.url}/environment`}
/>
</Switch>
</Modal>
@ -91,7 +87,8 @@ class DeploymentConfiguration extends React.Component {
}
}
DeploymentConfiguration.propTypes = {
location: PropTypes.object.isRequired
location: PropTypes.object.isRequired,
match: PropTypes.object.isRequired
};
export default checkRunningDeployment(DeploymentConfiguration);

View File

@ -41,29 +41,33 @@ const messages = defineMessages({
class DeploymentFailure extends React.Component {
render() {
const { formatMessage } = this.props.intl;
const status = formatMessage(
deploymentStatusMessages[this.props.stack.stack_status]
);
const {
currentPlanName,
deleteStack,
intl: { formatMessage },
isRequestingStackDelete,
stack
} = this.props;
const status = formatMessage(deploymentStatusMessages[stack.stack_status]);
return (
<div>
<InlineNotification type="error" title={status}>
<p>
{this.props.stack.stack_status_reason}
{stack.stack_status_reason}
{' '}
<Link to="/deployment-plan/deployment-detail">
<Link to={`/plans/${currentPlanName}/deployment-detail`}>
<FormattedMessage {...messages.moreDetails} />
</Link>
</p>
</InlineNotification>
<DeleteStackButton
content={formatMessage(messages.deleteDeployment)}
deleteStack={this.props.deleteStack}
disabled={this.props.isRequestingStackDelete}
loaded={!this.props.isRequestingStackDelete}
deleteStack={deleteStack}
disabled={isRequestingStackDelete}
loaded={!isRequestingStackDelete}
loaderContent={formatMessage(messages.requestingDeletion)}
stack={this.props.stack}
stack={stack}
/>
</div>
);
@ -71,6 +75,7 @@ class DeploymentFailure extends React.Component {
}
DeploymentFailure.propTypes = {
currentPlanName: PropTypes.string.isRequired,
deleteStack: PropTypes.func.isRequired,
intl: PropTypes.object,
isRequestingStackDelete: PropTypes.bool,

View File

@ -15,380 +15,36 @@
*/
import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
import { Route } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
import React, { Component } from 'react';
import DeploymentConfiguration from './DeploymentConfiguration';
import DeploymentDetail from '../deployment/DeploymentDetail';
import { getAllPlansButCurrent } from '../../selectors/plans';
import {
getCurrentStack,
getCurrentStackDeploymentProgress,
getCurrentStackDeploymentInProgress,
getOvercloudInfo
} from '../../selectors/stacks';
import {
getAvailableNodes,
getAvailableNodesCountsByRole,
getNodeCountParametersByRole,
getTotalAssignedNodesCount
} from '../../selectors/nodesAssignment';
import {
getEnvironmentConfigurationSummary
} from '../../selectors/environmentConfiguration';
import { getCurrentPlan } from '../../selectors/plans';
import { getRoles } from '../../selectors/roles';
import ConfigurePlanStep from './ConfigurePlanStep';
import CurrentPlanActions from '../../actions/CurrentPlanActions';
import { DeploymentPlanStep } from './DeploymentPlanStep';
import DeployStep from './DeployStep';
import EnvironmentConfigurationActions
from '../../actions/EnvironmentConfigurationActions';
import HardwareStep from './HardwareStep';
import PlansDropdown from './PlansDropdown';
import NodesActions from '../../actions/NodesActions';
import NoPlans from './NoPlans';
import NotificationActions from '../../actions/NotificationActions';
import ParametersActions from '../../actions/ParametersActions';
import PlansActions from '../../actions/PlansActions';
import RoleDetail from '../roles/RoleDetail';
import RolesStep from './RolesStep';
import RolesActions from '../../actions/RolesActions';
import StacksActions from '../../actions/StacksActions';
import stackStates from '../../constants/StacksConstants';
import ValidationsActions from '../../actions/ValidationsActions';
import CurrentPlan from './CurrentPlan';
import { getPlans } from '../../selectors/plans';
const messages = defineMessages({
hardwareStepHeader: {
id: 'DeploymentPlan.hardwareStepHeader',
defaultMessage: 'Prepare Hardware'
},
configureRolesStepHeader: {
id: 'DeploymentPlan.configureRolesStepHeader',
defaultMessage: 'Configure Roles and Assign Nodes'
},
deploymentConfigurationStepHeader: {
id: 'DeploymentPlan.deploymentConfigurationStepHeader',
defaultMessage: 'Specify Deployment Configuration'
},
deployStepHeader: {
id: 'DeploymentPlan.deployStepHeader',
defaultMessage: 'Deploy'
},
hardwareStepTooltip: {
id: 'DeploymentPlan.hardwareStepTooltip',
defaultMessage: 'This step registers and introspects your nodes. Registration involves ' +
'defining the power management details of each node so that you so that the director can ' +
'control them during the introspection and provisioning stages. After registration, you ' +
'introspect the nodes, which identifies the hardware each node uses and builds a profile of ' +
'each node. After registration and introspection, you can assign these nodes into specific ' +
'roles in your overcloud.'
},
configurePlanStepTooltip: {
id: 'DeploymentPlan.configurePlanStepTooltip',
defaultMessage: "This step allows you edit specific settings for the overcloud's network, " +
'storage, and other certified plugins. Use this step to define your network isolation ' +
'configuration and your backend storage settings.'
},
configureRolesStepTooltip: {
id: 'DeploymentPlan.configureRolesStepTooltip',
defaultMessage: 'This step assigns and removes nodes from roles in your overcloud. On each ' +
"role's selection dialog, you can tag available nodes into the role or untag nodes already " +
'assigned to the role. Click "Assign Nodes" for a particular role to open the selection ' +
'dialog and start assigning nodes. ' +
'You can also customize role-specific settings in this step. For example, you can compose ' +
'services on each role and customize specific parameters related to each role. Click the ' +
'pencil icon in the top-right corner of each role to see these role-specific settings'
},
deployStepTooltip: {
id: 'DeploymentPlan.deploymentStepTooltip',
defaultMessage: 'This step performs the deployment of the overcloud. Once the deployment ' +
'begins, the director tracks the progress and provides a report of each completed, running, ' +
'or failed step. When the deployment completes, the director displays the current overcloud ' +
'status and login details, which you use to interact with your overcloud. Click "Deploy" to ' +
'start the deployment.'
}
});
class DeploymentPlan extends React.Component {
componentDidMount() {
this.props.fetchStacks();
this.fetchParameters();
}
componentWillReceiveProps(nextProps) {
if (!nextProps.stacksLoaded) {
this.props.fetchStacks();
}
if (nextProps.currentPlan !== this.props.currentPlan) {
this.fetchParameters();
}
this.postDeploymentValidationsCheck(nextProps.currentStack);
this.pollCurrentStack(nextProps.currentStack);
}
componentWillUnmount() {
clearTimeout(this.stackProgressTimeout);
}
fetchParameters() {
!this.props.isFetchingParameters &&
this.props.fetchParameters(this.props.currentPlan.name);
}
pollCurrentStack(currentStack) {
if (currentStack) {
if (currentStack.stack_status.match(/PROGRESS/)) {
clearTimeout(this.stackProgressTimeout);
this.stackProgressTimeout = setTimeout(() => {
this.props.fetchStacks();
this.props.fetchStackResources(currentStack);
}, 20000);
}
}
}
postDeploymentValidationsCheck(nextStack) {
const { currentStack, currentPlan } = this.props;
const progressStates = [
stackStates.UPDATE_IN_PROGRESS,
stackStates.CREATE_IN_PROGRESS
];
const successStates = [
stackStates.UPDATE_COMPLETE,
stackStates.CREATE_COMPLETE
];
if (
currentStack &&
nextStack &&
progressStates.includes(currentStack.stack_status) &&
successStates.includes(nextStack.stack_status)
) {
this.props.runPostDeploymentValidations(currentPlan.name);
}
class DeploymentPlan extends Component {
componentWillMount() {
this.props.choosePlan(this.props.match.params.planName);
}
render() {
const { formatMessage } = this.props.intl;
const currentPlanName = this.props.hasPlans
? this.props.currentPlan.name
: undefined;
return (
<div className="row">
{this.props.hasPlans
? <div className="col-sm-12">
<div className="page-header page-header-bleed-right">
<h1>
{currentPlanName}
<PlansDropdown
currentPlanName={currentPlanName}
plans={this.props.inactivePlans}
choosePlan={this.props.choosePlan}
/>
</h1>
</div>
<ol className="deployment-step-list">
<DeploymentPlanStep
title={formatMessage(messages.hardwareStepHeader)}
disabled={this.props.currentStackDeploymentInProgress}
tooltip={formatMessage(messages.hardwareStepTooltip)}
>
<HardwareStep />
</DeploymentPlanStep>
<DeploymentPlanStep
title={formatMessage(
messages.deploymentConfigurationStepHeader
)}
disabled={this.props.currentStackDeploymentInProgress}
tooltip={formatMessage(messages.configurePlanStepTooltip)}
>
<ConfigurePlanStep
fetchEnvironmentConfiguration={
this.props.fetchEnvironmentConfiguration
}
summary={this.props.environmentConfigurationSummary}
planName={currentPlanName}
isFetching={this.props.isFetchingEnvironmentConfiguration}
loaded={this.props.environmentConfigurationLoaded}
/>
</DeploymentPlanStep>
<DeploymentPlanStep
title={formatMessage(messages.configureRolesStepHeader)}
disabled={this.props.currentStackDeploymentInProgress}
tooltip={formatMessage(messages.configureRolesStepTooltip)}
>
<RolesStep
nodeCountParametersByRole={
this.props.nodeCountParametersByRole
}
availableNodesCount={this.props.availableNodes.size}
availableNodesCountsByRole={
this.props.availableNodesCountsByRole
}
fetchNodes={this.props.fetchNodes}
fetchRoles={this.props.fetchRoles.bind(
this,
currentPlanName
)}
isFetchingNodes={this.props.isFetchingNodes}
isFetchingRoles={this.props.isFetchingRoles}
isFetchingParameters={this.props.isFetchingParameters}
roles={this.props.roles}
rolesLoaded={this.props.rolesLoaded}
totalAssignedNodesCount={this.props.totalAssignedNodesCount}
/>
</DeploymentPlanStep>
<DeploymentPlanStep
title={formatMessage(messages.deployStepHeader)}
tooltip={formatMessage(messages.deployStepTooltip)}
>
<DeployStep
currentPlan={this.props.currentPlan}
currentStack={this.props.currentStack}
currentStackResources={this.props.currentStackResources}
currentStackDeploymentProgress={
this.props.currentStackDeploymentProgress
}
deleteStack={this.props.deleteStack}
deployPlan={this.props.deployPlan}
fetchStackEnvironment={this.props.fetchStackEnvironment}
fetchStackResource={this.props.fetchStackResource}
overcloudInfo={this.props.overcloudInfo}
isRequestingStackDelete={this.props.isRequestingStackDelete}
stacksLoaded={this.props.stacksLoaded}
/>
</DeploymentPlanStep>
</ol>
</div>
: <div className="col-sm-12">
<NoPlans />
</div>}
<Route
path="/deployment-plan/configuration"
component={DeploymentConfiguration}
/>
<Route
path="/deployment-plan/roles/:roleIdentifier"
component={RoleDetail}
/>
<Route
path="/deployment-plan/deployment-detail"
component={DeploymentDetail}
/>
</div>
);
return this.props.currentPlan ? <CurrentPlan /> : <Redirect to="/plans" />;
}
}
DeploymentPlan.propTypes = {
availableNodes: ImmutablePropTypes.map,
availableNodesCountsByRole: ImmutablePropTypes.map.isRequired,
choosePlan: PropTypes.func,
choosePlan: PropTypes.func.isRequired,
currentPlan: ImmutablePropTypes.record,
currentStack: ImmutablePropTypes.record,
currentStackDeploymentInProgress: PropTypes.bool,
currentStackDeploymentProgress: PropTypes.number.isRequired,
currentStackResources: ImmutablePropTypes.map,
deleteStack: PropTypes.func,
deployPlan: PropTypes.func,
environmentConfigurationLoaded: PropTypes.bool,
environmentConfigurationSummary: PropTypes.string,
fetchEnvironmentConfiguration: PropTypes.func,
fetchNodes: PropTypes.func,
fetchParameters: PropTypes.func,
fetchRoles: PropTypes.func,
fetchStackEnvironment: PropTypes.func,
fetchStackResource: PropTypes.func,
fetchStackResources: PropTypes.func.isRequired,
fetchStacks: PropTypes.func,
hasPlans: PropTypes.bool,
inactivePlans: ImmutablePropTypes.map,
intl: PropTypes.object,
isFetchingEnvironmentConfiguration: PropTypes.bool,
isFetchingNodes: PropTypes.bool,
isFetchingParameters: PropTypes.bool,
isFetchingRoles: PropTypes.bool,
isRequestingStackDelete: PropTypes.bool,
nodeCountParametersByRole: ImmutablePropTypes.map,
notify: PropTypes.func,
overcloudInfo: ImmutablePropTypes.map.isRequired,
roles: ImmutablePropTypes.map,
rolesLoaded: PropTypes.bool,
route: PropTypes.object,
runPostDeploymentValidations: PropTypes.func.isRequired,
stacksLoaded: PropTypes.bool.isRequired,
totalAssignedNodesCount: PropTypes.number.isRequired
match: PropTypes.object.isRequired
};
export function mapStateToProps(state) {
return {
availableNodesCountsByRole: getAvailableNodesCountsByRole(state),
nodeCountParametersByRole: getNodeCountParametersByRole(state),
availableNodes: getAvailableNodes(state),
currentPlan: getCurrentPlan(state),
currentStack: getCurrentStack(state),
currentStackResources: state.stacks.resources,
currentStackDeploymentInProgress: getCurrentStackDeploymentInProgress(
state
),
currentStackDeploymentProgress: getCurrentStackDeploymentProgress(state),
environmentConfigurationLoaded: state.environmentConfiguration.loaded,
environmentConfigurationSummary: getEnvironmentConfigurationSummary(state),
isFetchingEnvironmentConfiguration: state.environmentConfiguration
.isFetching,
isFetchingNodes: state.nodes.get('isFetching'),
isFetchingParameters: state.parameters.isFetching,
isFetchingRoles: state.roles.get('isFetching'),
overcloudInfo: getOvercloudInfo(state),
isRequestingStackDelete: state.stacks.get('isRequestingStackDelete'),
hasPlans: !state.plans.get('all').isEmpty(),
inactivePlans: getAllPlansButCurrent(state),
roles: getRoles(state),
rolesLoaded: state.roles.get('loaded'),
stacksLoaded: state.stacks.get('isLoaded'),
totalAssignedNodesCount: getTotalAssignedNodesCount(state)
};
}
const mapStateToProps = (state, props) => ({
currentPlan: getPlans(state).get(props.match.params.planName)
});
function mapDispatchToProps(dispatch) {
return {
choosePlan: planName => dispatch(CurrentPlanActions.choosePlan(planName)),
deleteStack: (stackName, stackId) => {
dispatch(StacksActions.deleteStack(stackName, stackId));
},
deployPlan: planName => dispatch(PlansActions.deployPlan(planName)),
fetchStackEnvironment: stack =>
dispatch(StacksActions.fetchEnvironment(stack)),
fetchEnvironmentConfiguration: (planName, parentPath) => {
dispatch(
EnvironmentConfigurationActions.fetchEnvironmentConfiguration(
planName,
parentPath
)
);
},
fetchNodes: () => dispatch(NodesActions.fetchNodes()),
fetchParameters: planName =>
dispatch(ParametersActions.fetchParameters(planName)),
fetchRoles: planName => dispatch(RolesActions.fetchRoles(planName)),
fetchStackResources: stack =>
dispatch(StacksActions.fetchResources(stack.stack_name, stack.id)),
fetchStackResource: (stack, resourceName) =>
dispatch(StacksActions.fetchResource(stack, resourceName)),
fetchStacks: () => dispatch(StacksActions.fetchStacks()),
notify: notification => dispatch(NotificationActions.notify(notification)),
runPostDeploymentValidations: planName => {
dispatch(
ValidationsActions.runValidationGroups(['post-deployment'], planName)
);
}
};
}
const mapDispatchToProps = (dispatch, props) => ({
choosePlan: planName => dispatch(CurrentPlanActions.choosePlan(planName))
});
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(DeploymentPlan)
);
export default connect(mapStateToProps, mapDispatchToProps)(DeploymentPlan);

View File

@ -59,24 +59,29 @@ class DeploymentProgress extends React.Component {
}
render() {
const { formatMessage } = this.props.intl;
const {
currentPlanName,
deleteStack,
intl: { formatMessage },
isRequestingStackDelete,
stack
} = this.props;
const statusMessage = (
<strong>
<FormattedMessage {...statusMessages[this.props.stack.stack_status]} />
<FormattedMessage {...statusMessages[stack.stack_status]} />
</strong>
);
const deleteButton = this.props.stack.stack_status !==
stackStates.DELETE_IN_PROGRESS
const deleteButton = stack.stack_status !== stackStates.DELETE_IN_PROGRESS
? <DeleteStackButton
content={formatMessage(messages.cancelDeployment)}
buttonIconClass="fa fa-ban"
deleteStack={this.props.deleteStack}
disabled={this.props.isRequestingStackDelete}
loaded={!this.props.isRequestingStackDelete}
deleteStack={deleteStack}
disabled={isRequestingStackDelete}
loaded={!isRequestingStackDelete}
loaderContent={formatMessage(messages.requestingDeletion)}
stack={this.props.stack}
stack={stack}
/>
: null;
@ -84,7 +89,7 @@ class DeploymentProgress extends React.Component {
<div>
<p>
<span><FormattedMessage {...messages.deploymentInProgress} /> </span>
<Link to="/deployment-plan/deployment-detail">
<Link to={`/plans/${currentPlanName}/deployment-detail`}>
<FormattedMessage {...messages.viewInformation} />
</Link>
</p>
@ -99,6 +104,7 @@ class DeploymentProgress extends React.Component {
}
DeploymentProgress.propTypes = {
currentPlanName: PropTypes.string.isRequired,
deleteStack: PropTypes.func.isRequired,
deploymentProgress: PropTypes.number.isRequired,
intl: PropTypes.object,

View File

@ -43,7 +43,7 @@ export default class NoPlans extends React.Component {
<h1><FormattedMessage {...messages.noPlansAvailable} /></h1>
<p><FormattedMessage {...messages.noPlansAvailableMessage} /></p>
<div className="blank-slate-pf-main-action">
<Link to="/plans/new" className="btn btn-lg btn-primary">
<Link to="/plans/manage/new" className="btn btn-lg btn-primary">
<span className="fa fa-plus" />
{' '}
<FormattedMessage {...messages.createNewPlan} />

View File

@ -1,78 +0,0 @@
/**
* Copyright 2017 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { defineMessages, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
import DropdownItem from '../ui/dropdown/DropdownItem';
import DropdownButton from '../ui/dropdown/DropdownButton';
import Dropdown from '../ui/dropdown/Dropdown';
const messages = defineMessages({
manageDeployments: {
id: 'PlansDropdown.manageDeployments',
defaultMessage: 'Manage Deployments'
},
selectDeployment: {
id: 'PlansDropdown.selectDeployment',
defaultMessage: 'Select Deployment'
}
});
export default class PlansDropdown extends React.Component {
renderRecentPlans() {
return this.props.plans.toList().map(plan => {
return (
<DropdownItem
key={plan.name}
onClick={this.props.choosePlan.bind(this, plan.name)}
>
{plan.name}
</DropdownItem>
);
});
}
render() {
if (this.props.plans.isEmpty()) {
return (
<Link className="btn btn-link" to="/plans">
<FormattedMessage {...messages.manageDeployments} />
</Link>
);
} else {
return (
<Dropdown>
<DropdownButton className="btn-link">
<FormattedMessage {...messages.selectDeployment} />
</DropdownButton>
{this.renderRecentPlans()}
<DropdownItem key="divider" divider />
<DropdownItem key="plansLink" to="/plans">
<FormattedMessage {...messages.manageDeployments} />
</DropdownItem>
</Dropdown>
);
}
}
}
PlansDropdown.propTypes = {
choosePlan: PropTypes.func,
plans: ImmutablePropTypes.map
};

View File

@ -48,6 +48,7 @@ const messages = defineMessages({
const RoleCard = ({
assignedNodesCountParameter,
availableNodesCount,
currentPlanName,
identifier,
intl,
name,
@ -69,7 +70,7 @@ const RoleCard = ({
<h2 className="card-pf-title">
{title}
<Link
to={`deployment-plan/roles/${identifier}`}
to={`/plans/${currentPlanName}/roles/${identifier}`}
className="link pull-right"
title="Edit Role parameters"
>
@ -114,6 +115,7 @@ const RoleCard = ({
RoleCard.propTypes = {
assignedNodesCountParameter: ImmutablePropTypes.record,
availableNodesCount: PropTypes.number.isRequired,
currentPlanName: PropTypes.string.isRequired,
identifier: PropTypes.string.isRequired,
intl: PropTypes.object,
name: PropTypes.string.isRequired,

View File

@ -48,6 +48,7 @@ class Roles extends React.Component {
return (
<div className="col-xs-6 col-sm-4 col-md-3 col-lg-2" key={role.name}>
<RoleCard
currentPlanName={this.props.currentPlanName}
name={role.name}
title={role.title}
identifier={role.identifier}
@ -88,6 +89,7 @@ class Roles extends React.Component {
}
Roles.propTypes = {
availableNodesCountsByRole: ImmutablePropTypes.map.isRequired,
currentPlanName: PropTypes.string.isRequired,
fetchNodes: PropTypes.func.isRequired,
fetchRoles: PropTypes.func.isRequired,
intl: PropTypes.object,

View File

@ -40,6 +40,7 @@ const messages = defineMessages({
const RolesStep = ({
availableNodesCount,
availableNodesCountsByRole,
currentPlanName,
roles,
fetchRoles,
fetchNodes,
@ -79,6 +80,7 @@ const RolesStep = ({
</Loader>
</p>
<Roles
currentPlanName={currentPlanName}
roles={roles.toList().toJS()}
availableNodesCountsByRole={availableNodesCountsByRole}
nodeCountParametersByRole={nodeCountParametersByRole}
@ -94,6 +96,7 @@ const RolesStep = ({
RolesStep.propTypes = {
availableNodesCount: PropTypes.number.isRequired,
availableNodesCountsByRole: ImmutablePropTypes.map.isRequired,
currentPlanName: PropTypes.string.isRequired,
fetchNodes: PropTypes.func.isRequired,
fetchRoles: PropTypes.func.isRequired,
intl: PropTypes.object,

View File

@ -26,6 +26,7 @@ import React from 'react';
import EnvironmentConfigurationActions
from '../../actions/EnvironmentConfigurationActions';
import EnvironmentConfigurationTopic from './EnvironmentConfigurationTopic';
import { getCurrentPlanName } from '../../selectors/plans';
import ModalFormErrorList from '../ui/forms/ModalFormErrorList';
import { getTopicsTree } from '../../selectors/environmentConfiguration';
import Loader from '../ui/Loader';
@ -58,8 +59,13 @@ class EnvironmentConfiguration extends React.Component {
}
componentDidMount() {
this.props.fetchEnvironmentConfiguration(this.props.currentPlanName, () =>
this.props.history.push('/deployment-plan')
const {
currentPlanName,
fetchEnvironmentConfiguration,
history
} = this.props;
fetchEnvironmentConfiguration(currentPlanName, () =>
history.push(`/plans/${currentPlanName}`)
);
}
@ -110,7 +116,7 @@ class EnvironmentConfiguration extends React.Component {
closeOnSubmit: false
});
this.props.history.push('/deployment-plan');
this.props.history.push(`/plans/${this.props.currentPlanName}`);
}
}
@ -209,7 +215,11 @@ class EnvironmentConfiguration extends React.Component {
>
<FormattedMessage {...messages.saveAndClose} />
</button>
<Link to="/deployment-plan" type="button" className="btn btn-default">
<Link
to={`/plans/${this.props.currentPlanName}`}
type="button"
className="btn btn-default"
>
<FormattedMessage {...messages.cancel} />
</Link>
</div>
@ -232,7 +242,7 @@ EnvironmentConfiguration.propTypes = {
function mapStateToProps(state) {
return {
currentPlanName: state.currentPlan.currentPlanName,
currentPlanName: getCurrentPlanName(state),
environmentConfigurationTopics: getTopicsTree(state),
formErrors: state.environmentConfiguration.getIn(['form', 'formErrors']),
formFieldErrors: state.environmentConfiguration.getIn([

View File

@ -32,7 +32,6 @@ import NodesListView from './NodesListView/NodesListView';
import NodesToolbar from './NodesToolbar/NodesToolbar';
import NodesTableView from './NodesTableView';
import RegisterNodesDialog from './RegisterNodesDialog';
import RolesActions from '../../actions/RolesActions';
const messages = defineMessages({
loadingNodes: {
@ -56,13 +55,11 @@ const messages = defineMessages({
class Nodes extends React.Component {
componentDidMount() {
this.props.fetchNodes();
this.props.fetchRoles(this.props.currentPlanName);
}
refreshResults(e) {
e.preventDefault();
this.props.fetchNodes();
this.props.fetchRoles(this.props.currentPlanName);
}
renderContentView() {
@ -120,10 +117,8 @@ class Nodes extends React.Component {
}
Nodes.propTypes = {
contentView: PropTypes.string.isRequired,
currentPlanName: PropTypes.string.isRequired,
fetchNodeIntrospectionData: PropTypes.func.isRequired,
fetchNodes: PropTypes.func.isRequired,
fetchRoles: PropTypes.func.isRequired,
fetchingNodes: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired,
nodes: ImmutablePropTypes.map.isRequired,
@ -136,7 +131,6 @@ const mapStateToProps = state => ({
'contentView',
'list'
),
currentPlanName: state.currentPlan.currentPlanName,
fetchingNodes: state.nodes.get('isFetching'),
nodes: getFilteredNodes(state),
nodesInProgress: nodesInProgress(state),
@ -146,9 +140,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
fetchNodes: () => dispatch(NodesActions.fetchNodes()),
fetchNodeIntrospectionData: nodeId =>
dispatch(NodesActions.fetchNodeIntrospectionData(nodeId)),
fetchRoles: currentPlanName =>
dispatch(RolesActions.fetchRoles(currentPlanName))
dispatch(NodesActions.fetchNodeIntrospectionData(nodeId))
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Nodes));

View File

@ -21,6 +21,7 @@ import React from 'react';
import EnvironmentConfigurationActions
from '../../actions/EnvironmentConfigurationActions';
import { getCurrentPlanName } from '../../selectors/plans';
import { getEnvironmentParameters } from '../../selectors/parameters';
import { getEnvironment } from '../../selectors/environmentConfiguration';
import InlineNotification from '../ui/InlineNotification';
@ -65,7 +66,7 @@ EnvironmentParameters.propTypes = {
function mapStateToProps(state, ownProps) {
return {
currentPlanName: state.currentPlan.currentPlanName,
currentPlanName: getCurrentPlanName(state),
environmentError: getEnvironment(state, ownProps.environment).error,
parameters: getEnvironmentParameters(state, ownProps.environment),
parametersLoaded: state.parameters.loaded,

View File

@ -27,6 +27,7 @@ import React from 'react';
import EnvironmentConfigurationActions
from '../../actions/EnvironmentConfigurationActions';
import EnvironmentParameters from './EnvironmentParameters';
import { getCurrentPlanName } from '../../selectors/plans';
import { getRootParameters } from '../../selectors/parameters';
import {
getEnabledEnvironments
@ -134,7 +135,7 @@ class Parameters extends React.Component {
closeOnSubmit: false
});
this.props.history.push('/deployment-plan');
this.props.history.push(`/plans/${this.props.currentPlanName}`);
}
}
@ -259,7 +260,11 @@ class Parameters extends React.Component {
>
<FormattedMessage {...messages.saveAndClose} />
</button>
<Link to="/deployment-plan" type="button" className="btn btn-default">
<Link
to={`/plans/${this.props.currentPlanName}`}
type="button"
className="btn btn-default"
>
<FormattedMessage {...messages.cancel} />
</Link>
</div>
@ -290,6 +295,7 @@ function mapStateToProps(state, ownProps) {
form: state.parameters.form,
formErrors: state.parameters.form.get('formErrors'),
formFieldErrors: state.parameters.form.get('formFieldErrors'),
currentPlanName: getCurrentPlanName(state),
isFetchingParameters: state.parameters.isFetching,
mistralParameters: state.parameters.mistralParameters,
parameters: getRootParameters(state),
@ -303,14 +309,14 @@ function mapDispatchToProps(dispatch, ownProps) {
dispatch(
EnvironmentConfigurationActions.fetchEnvironmentConfiguration(
currentPlanName,
() => ownProps.history.push('/deployment-plan')
() => ownProps.history.push(`/plans/${currentPlanName}`)
)
);
},
fetchParameters: currentPlanName => {
dispatch(
ParametersActions.fetchParameters(currentPlanName, () =>
ownProps.history.push('/deployment-plan')
ownProps.history.push(`/plans/${currentPlanName}`)
)
);
},

View File

@ -59,7 +59,7 @@ class DeletePlan extends React.Component {
return (
<Modal dialogClasses="modal-sm" id="DeletePlan__deletePlanModal">
<div className="modal-header">
<Link to="/plans" type="button" className="close">
<Link to="/plans/manage" type="button" className="close">
<span aria-hidden="true" className="pficon pficon-close" />
</Link>
<h4 className="modal-title">
@ -97,7 +97,7 @@ class DeletePlan extends React.Component {
<FormattedMessage {...messages.deletePlan} />
</button>
<Link
to="/plans"
to="/plans/manage"
type="button"
className="btn btn-default"
id="DeletePlan__cancelDeletePlanModalButton"

View File

@ -116,7 +116,7 @@ class EditPlan extends React.Component {
onInvalid={this.onFormInvalid.bind(this)}
>
<div className="modal-header">
<Link to="/plans" type="button" className="close">
<Link to="/plans/manage" type="button" className="close">
<span aria-hidden="true" className="pficon pficon-close" />
</Link>
<h4>
@ -151,7 +151,7 @@ class EditPlan extends React.Component {
>
<FormattedMessage {...messages.uploadAndUpdate} />
</button>
<Link to="/plans" type="button" className="btn btn-default">
<Link to="/plans/manage" type="button" className="btn btn-default">
<FormattedMessage {...messages.cancel} />
</Link>
</div>

View File

@ -68,7 +68,7 @@ class ExportPlan extends React.Component {
return (
<Modal dialogClasses="modal-sm" id="ExportPlan__exportPlanModal">
<div className="modal-header">
<Link to="/plans" type="button" className="close">
<Link to="/plans/manage" type="button" className="close">
<span aria-hidden="true" className="pficon pficon-close" />
</Link>
<h4 className="modal-title">
@ -107,7 +107,7 @@ class ExportPlan extends React.Component {
</div>
<div className="modal-footer">
<Link
to="/plans"
to="/plans/manage"
type="button"
className="btn btn-default"
id="ExportPlan__cancelExportPlanModalButton"

View File

@ -20,7 +20,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
import CurrentPlanActions from '../../actions/CurrentPlanActions';
import { getCurrentPlanName, getPlans } from '../../selectors/plans';
import { PageHeader } from '../ui/PageHeader';
import PlansActions from '../../actions/PlansActions';
import StacksActions from '../../actions/StacksActions';
@ -50,7 +50,6 @@ class ListPlans extends React.Component {
<PlanCard
key={plan.name}
plan={plan}
choosePlan={this.props.choosePlan}
currentPlanName={this.props.currentPlanName}
stack={stack}
/>
@ -83,8 +82,6 @@ class ListPlans extends React.Component {
ListPlans.propTypes = {
children: PropTypes.node,
choosePlan: PropTypes.func,
conflict: PropTypes.string,
currentPlanName: PropTypes.string,
fetchPlans: PropTypes.func,
fetchStacks: PropTypes.func,
@ -94,8 +91,8 @@ ListPlans.propTypes = {
function mapStateToProps(state) {
return {
currentPlanName: state.currentPlan.currentPlanName,
plans: state.plans.get('all'),
currentPlanName: getCurrentPlanName(state),
plans: getPlans(state),
stacks: state.stacks.get('stacks')
};
}
@ -103,8 +100,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
fetchPlans: () => dispatch(PlansActions.fetchPlans()),
fetchStacks: () => dispatch(StacksActions.fetchStacks()),
choosePlan: planName => dispatch(CurrentPlanActions.choosePlan(planName))
fetchStacks: () => dispatch(StacksActions.fetchStacks())
};
}

View File

@ -107,7 +107,7 @@ class NewPlan extends React.Component {
>
<div className="modal-header">
<Link
to="/plans"
to="/plans/manage"
type="button"
onClick={() => this.props.cancelCreatePlan()}
className="close"
@ -142,7 +142,7 @@ class NewPlan extends React.Component {
<FormattedMessage {...messages.uploadAndCreate} />
</button>
<Link
to="/plans"
to="/plans/manage"
type="button"
onClick={() => this.props.cancelCreatePlan()}
className="btn btn-default"

View File

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

View File

@ -30,7 +30,7 @@ const CreatePlanCard = () => (
<div className="card-pf">
<div className="card-pf-body">
<span className="pficon pficon-add-circle-o" />&nbsp;
<Link to="/plans/new" id="ListPlans__newPlanLink">
<Link to="/plans/manage/new" id="ListPlans__newPlanLink">
<FormattedMessage {...messages.createNewPlan} />
</Link>
</div>

View File

@ -16,7 +16,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import {
defineMessages,
FormattedMessage,
@ -56,11 +56,6 @@ const messages = defineMessages({
});
class PlanCard extends React.Component {
onPlanClick(e) {
e.preventDefault();
this.props.choosePlan(e.target.textContent);
}
getActiveIcon() {
if (this.props.plan.name === this.props.currentPlanName) {
return <span className="pficon pficon-flag" />;
@ -78,9 +73,9 @@ class PlanCard extends React.Component {
);
} else {
return (
<a href="" onClick={this.onPlanClick.bind(this)}>
<Link to={`/plans/${this.props.plan.name}`}>
{this.props.plan.name}
</a>
</Link>
);
}
}
@ -158,14 +153,14 @@ class PlanCard extends React.Component {
&nbsp;
{this.getActiveIcon()}
<div className="pull-right">
<DropdownKebab id={`card-actions-${planName}`}>
<MenuItemLink to={`/plans/${planName}/edit`}>
<DropdownKebab id={`card-actions-${planName}`} pullRight>
<MenuItemLink to={`/plans/manage/${planName}/edit`}>
<FormattedMessage {...messages.edit} />
</MenuItemLink>
<MenuItemLink to={`/plans/${planName}/export`}>
<MenuItemLink to={`/plans/manage/${planName}/export`}>
<FormattedMessage {...messages.export} />
</MenuItemLink>
<MenuItemLink to={`/plans/${planName}/delete`}>
<MenuItemLink to={`/plans/manage/${planName}/delete`}>
<FormattedMessage {...messages.delete} />
</MenuItemLink>
</DropdownKebab>
@ -182,12 +177,10 @@ class PlanCard extends React.Component {
}
PlanCard.propTypes = {
choosePlan: PropTypes.func,
currentPlanName: PropTypes.string,
history: PropTypes.object,
intl: PropTypes.object,
plan: PropTypes.object,
stack: PropTypes.object
};
export default withRouter(injectIntl(PlanCard));
export default injectIntl(PlanCard);

View File

@ -25,6 +25,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { checkRunningDeployment } from '../utils/checkRunningDeploymentHOC';
import { getCurrentPlanName } from '../../selectors/plans';
import { getRole } from '../../selectors/roles';
import { getRoleServices } from '../../selectors/parameters';
import Loader from '../ui/Loader';
@ -78,7 +79,8 @@ class RoleDetail extends React.Component {
}
componentDidMount() {
this.props.fetchParameters(this.props.currentPlanName, '/deployment-plan');
const { currentPlanName } = this.props;
this.props.fetchParameters(currentPlanName, `/plans/${currentPlanName}`);
}
componentDidUpdate() {
@ -139,8 +141,8 @@ class RoleDetail extends React.Component {
renderRoleTabs() {
const {
currentPlanName,
formErrors,
location,
match: { params: urlParams },
parametersLoaded,
rolesLoaded
@ -150,23 +152,17 @@ class RoleDetail extends React.Component {
<div className="row">
<ul className="nav nav-tabs">
<NavTab
location={location}
to={`/deployment-plan/roles/${urlParams.roleIdentifier}/parameters`}
to={`/plans/${currentPlanName}/roles/${urlParams.roleIdentifier}/parameters`}
>
<FormattedMessage {...messages.parameters} />
</NavTab>
<NavTab
location={location}
to={`/deployment-plan/roles/${urlParams.roleIdentifier}/services`}
to={`/plans/${currentPlanName}/roles/${urlParams.roleIdentifier}/services`}
>
<FormattedMessage {...messages.services} />
</NavTab>
<NavTab
location={location}
to={
`/deployment-plan/roles/${urlParams.roleIdentifier}` +
'/network-configuration'
}
to={`/plans/${currentPlanName}/roles/${urlParams.roleIdentifier}/network-configuration`}
>
<FormattedMessage {...messages.networkConfiguration} />
</NavTab>
@ -180,7 +176,12 @@ class RoleDetail extends React.Component {
render() {
const dataLoaded = this.props.rolesLoaded && this.props.parametersLoaded;
const roleName = this.props.role ? this.props.role.name : null;
const { match: { params: urlParams } } = this.props;
const {
currentPlanName,
intl,
location,
match: { params: urlParams }
} = this.props;
return (
<Formsy.Form
@ -194,7 +195,11 @@ class RoleDetail extends React.Component {
<ModalPanelBackdrop />
<ModalPanel>
<ModalPanelHeader>
<Link to="/deployment-plan" type="button" className="close">
<Link
to={`/plans/${currentPlanName}`}
type="button"
className="close"
>
<span aria-hidden="true" className="pficon pficon-close" />
</Link>
<h2 className="modal-title">
@ -208,27 +213,25 @@ class RoleDetail extends React.Component {
<ModalPanelBody>
<Loader
height={60}
content={this.props.intl.formatMessage(
messages.loadingParameters
)}
content={intl.formatMessage(messages.loadingParameters)}
loaded={dataLoaded}
>
<Switch>
<Switch location={location}>
<Route
path="/deployment-plan/roles/:roleIdentifier/parameters"
path="/plans/:planName/roles/:roleIdentifier/parameters"
component={RoleParameters}
/>
<Route
path="/deployment-plan/roles/:roleIdentifier/services"
path="/plans/:planName/roles/:roleIdentifier/services"
component={RoleServices}
/>
<Route
path="/deployment-plan/roles/:roleIdentifier/network-configuration"
path="/plans/:planName/roles/:roleIdentifier/network-configuration"
component={RoleNetworkConfig}
/>
<Redirect
from="/deployment-plan/roles/:roleIdentifier"
to={`/deployment-plan/roles/${urlParams.roleIdentifier}/parameters`}
from="/plans/:planName/roles/:roleIdentifier"
to={`/plans/${currentPlanName}/roles/${urlParams.roleIdentifier}/parameters`}
/>
</Switch>
</Loader>
@ -266,7 +269,7 @@ RoleDetail.propTypes = {
function mapStateToProps(state, props) {
return {
currentPlanName: state.currentPlan.currentPlanName,
currentPlanName: getCurrentPlanName(state),
formErrors: state.parameters.form.get('formErrors'),
formFieldErrors: state.parameters.form.get('formFieldErrors'),
allParameters: state.parameters.parameters,

View File

@ -19,9 +19,7 @@ import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import React from 'react';
import {
currentPlanNameSelector as getCurrentPlanName
} from '../../selectors/plans';
import { getCurrentPlanName } from '../../selectors/plans';
import { getCurrentStackDeploymentInProgress } from '../../selectors/stacks';
import NotificationActions from '../../actions/NotificationActions';
@ -39,7 +37,7 @@ export const checkRunningDeployment = WrappedComponent => {
render() {
return this.props.currentStackDeploymentInProgress
? <Redirect to="/deployment-plan" />
? <Redirect to={`/plans/${this.props.currentPlanName}`} />
: <WrappedComponent {...this.props} />;
}
}

View File

@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import BlankSlate from '../ui/BlankSlate';
import { getCurrentPlanName } from '../../selectors/plans';
import Loader from '../ui/Loader';
import ValidationsActions from '../../actions/ValidationsActions';
import ValidationsToolbar from './ValidationsToolbar';
@ -222,7 +223,7 @@ const mapStateToProps = state => {
isFetchingValidations: state.validations.get('isFetching'),
validations: getFilteredValidations(state),
validationsLoaded: state.validations.get('validationsLoaded'),
currentPlanName: state.currentPlan.currentPlanName
currentPlanName: getCurrentPlanName(state)
};
};

View File

@ -24,7 +24,6 @@ export default keyMirror({
REQUEST_PLAN_DELETE: null,
RECEIVE_PLAN_DELETE: null,
PLAN_CHOSEN: null,
PLAN_DETECTED: null,
DELETE_PLAN_PENDING: null,
DELETE_PLAN_SUCCESS: null,
DELETE_PLAN_FAILED: null,

View File

@ -17,6 +17,5 @@
import { Record } from 'immutable';
export const CurrentPlanState = Record({
conflict: undefined,
currentPlanName: undefined
});

View File

@ -24,11 +24,6 @@ export default function currentPlanReducer(state = initialState, action) {
case PlansConstants.PLAN_CHOSEN:
return state.set('currentPlanName', action.payload);
case PlansConstants.PLAN_DETECTED:
return state
.set('currentPlanName', action.payload.currentPlanName)
.set('conflict', action.payload.conflict);
default:
return state;
}

View File

@ -16,22 +16,28 @@
import { createSelector } from 'reselect';
const plansSelector = state => state.plans.get('all').sortBy(plan => plan.name);
export const plans = state => state.plans.get('all').sortBy(plan => plan.name);
export const currentPlanName = state => state.currentPlan.currentPlanName;
export const currentPlanNameSelector = state =>
state.currentPlan.get('currentPlanName');
export const getCurrentPlan = createSelector(
plansSelector,
currentPlanNameSelector,
plans,
currentPlanName,
(plans, currentPlanName) => plans.get(currentPlanName)
);
export const getCurrentPlanName = createSelector(
getCurrentPlan,
currentPlan => currentPlan && currentPlan.name
);
export const getPlans = createSelector(plans, plans => plans);
/**
* Returns a Map o all plans except for the selected one
*/
// TODO(jtomasek): update this to list 3 last used plans
export const getAllPlansButCurrent = createSelector(
[plansSelector, currentPlanNameSelector],
[plans, getCurrentPlanName],
(plans, currentPlanName) => {
return plans
.filter(plan => plan.name != currentPlanName)

View File

@ -18,7 +18,7 @@ import { createSelector } from 'reselect';
import { Map } from 'immutable';
import { Stack } from '../immutableRecords/stacks';
import { currentPlanNameSelector } from './plans';
import { getCurrentPlanName } from './plans';
const stacksSelector = state => state.stacks.stacks;
const currentStackEnvironmentSelector = state =>
@ -30,7 +30,7 @@ const stackResourceDetailsSelector = state => state.stacks.resourceDetails;
* Returns the stack associated with currentPlanName
*/
export const getCurrentStack = createSelector(
[stacksSelector, currentPlanNameSelector],
[stacksSelector, getCurrentPlanName],
(stacks, currentPlanName) => stacks.get(currentPlanName)
);
@ -39,7 +39,7 @@ export const getCurrentStack = createSelector(
* (true if the plan is currently being deployed, false it not).
*/
export const getCurrentStackDeploymentInProgress = createSelector(
[stacksSelector, currentPlanNameSelector],
[stacksSelector, getCurrentPlanName],
(stacks, currentPlanName) => {
return (
stacks.get(currentPlanName, new Stack()).stack_status ===
@ -71,7 +71,7 @@ export const getCurrentStackDeploymentProgress = createSelector(
export const getOvercloudInfo = createSelector(
[
currentStackEnvironmentSelector,
currentPlanNameSelector,
getCurrentPlanName,
stackResourceDetailsSelector
],
(currentStackEnvironment, currentPlanName, stackResourceDetails) => {

View File

@ -16,7 +16,7 @@
import { createSelector } from 'reselect';
import { currentPlanNameSelector } from './plans';
import { getCurrentPlanName } from './plans';
import { getFilterByName } from './filters';
import MistralConstants from '../constants/MistralConstants';
@ -29,7 +29,7 @@ const validationsToolbarFilter = state =>
* Filter workflow executions only to validations and current plan
*/
export const getValidationExecutionsForCurrentPlan = createSelector(
[executions, currentPlanNameSelector],
[executions, getCurrentPlanName],
(executions, currentPlanName) => {
return executions.filter(
execution =>

View File

@ -20,8 +20,23 @@ import createLogger from 'redux-logger';
import logger from './services/logger';
import appReducer from './reducers/appReducer';
import { CurrentPlanState } from './immutableRecords/currentPlan';
import { getIntl } from './selectors/i18n';
const hydrateStore = () => {
return {
currentPlan: new CurrentPlanState({
currentPlanName: getStoredPlanName()
})
};
};
function getStoredPlanName() {
if (window && window.localStorage) {
return window.localStorage.getItem('currentPlanName');
}
}
const loggerMiddleware = createLogger({
collapsed: true,
logger: logger
@ -29,7 +44,7 @@ const loggerMiddleware = createLogger({
const store = createStore(
appReducer,
{},
hydrateStore(),
applyMiddleware(
thunkMiddleware.withExtraArgument({ getIntl }),
loggerMiddleware

View File

@ -166,3 +166,9 @@
// text-overflow: clip;
// white-space: normal;
// }
// Remove top margin from page-header if there are breadcrumbs above it
.breadcrumb + .page-header {
margin-top: 0;
}