Update code to React Router v4 API

* Move from route config to Route components in container components
* Reorganize Authentication functionality to use <Redirect> components
* Move Zaqar connection init from login actions to
  AuthenticatedContent component so history object can be passed to
  ZaqarActions
* Update Deployment Plan, Plans, Roles and Deployment Configuration
  routes
* Add checkRunningDeployment HOC to handle redirecting when Plan
  deployment is in progress

Change-Id: Iaa87a710e3ed57f7bbf41d948583fc5c0b777cec
This commit is contained in:
Jiri Tomasek 2017-05-12 10:41:30 +02:00
parent 169b7171b8
commit 8302f53ba7
48 changed files with 630 additions and 535 deletions

View File

@ -19,7 +19,6 @@ import when from 'when';
import * as utils from '../../js/services/utils';
import EnvironmentConfigurationActions
from '../../js/actions/EnvironmentConfigurationActions';
import { browserHistory } from 'react-router';
import MistralApiService from '../../js/services/MistralApiService';
import { mockGetIntl } from './utils';
@ -79,7 +78,6 @@ describe('EnvironmentConfigurationActions', () => {
EnvironmentConfigurationActions,
'updateEnvironmentConfigurationSuccess'
);
spyOn(browserHistory, 'push');
// Mock the service call.
spyOn(MistralApiService, 'runAction').and.callFake(
createResolvingPromise({
@ -92,8 +90,7 @@ describe('EnvironmentConfigurationActions', () => {
EnvironmentConfigurationActions.updateEnvironmentConfiguration(
'overcloud',
{},
{},
'/redirect/url'
{}
)(() => {}, () => {}, mockGetIntl);
// Call done with a minimal timeout.
setTimeout(() => {
@ -112,9 +109,5 @@ describe('EnvironmentConfigurationActions', () => {
EnvironmentConfigurationActions.updateEnvironmentConfigurationSuccess
).toHaveBeenCalled();
});
it('redirects the page', () => {
expect(browserHistory.push).toHaveBeenCalledWith('/redirect/url');
});
});
});

View File

@ -18,6 +18,7 @@ import when from 'when';
import * as utils from '../../js/services/utils';
import MistralApiService from '../../js/services/MistralApiService';
import mockHistory from '../mocks/history';
import { mockGetIntl } from './utils';
import PlansActions from '../../js/actions/PlansActions';
import SwiftApiService from '../../js/services/SwiftApiService';
@ -49,7 +50,11 @@ describe('PlansActions', () => {
);
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
PlansActions.updatePlan('somecloud', {})(() => {}, () => {}, mockGetIntl);
PlansActions.updatePlan('somecloud', {}, mockHistory)(
() => {},
() => {},
mockGetIntl
);
// Call done with a minimal timeout.
setTimeout(() => {
done();
@ -108,7 +113,11 @@ describe('PlansActions', () => {
);
// Call the action creator and the resulting action.
// In this case, dispatch and getState are just empty placeHolders.
PlansActions.deletePlan('somecloud')(() => {}, () => {}, mockGetIntl);
PlansActions.deletePlan('somecloud', mockHistory)(
() => {},
() => {},
mockGetIntl
);
// Call done with a minimal timeout.
setTimeout(() => {
done();

View File

@ -14,11 +14,11 @@
* under the License.
*/
import { browserHistory } from 'react-router';
import when from 'when';
import { Map } from 'immutable';
import MistralApiService from '../../js/services/MistralApiService';
import mockHistory from '../mocks/history';
import { mockGetIntl } from './utils';
import RegisterNodesActions from '../../js/actions/RegisterNodesActions';
import NodesActions from '../../js/actions/NodesActions';
@ -107,9 +107,8 @@ describe('nodesRegistrationFinished', () => {
spyOn(NotificationActions, 'notify');
spyOn(RegisterNodesActions, 'nodesRegistrationSuccess');
spyOn(browserHistory, 'push');
RegisterNodesActions.nodesRegistrationFinished(messagePayload)(
RegisterNodesActions.nodesRegistrationFinished(messagePayload, mockHistory)(
() => {},
() => {
return {
@ -129,7 +128,7 @@ describe('nodesRegistrationFinished', () => {
successNotification
);
expect(RegisterNodesActions.nodesRegistrationSuccess).toHaveBeenCalled();
expect(browserHistory.push).toHaveBeenCalledWith('/nodes');
expect(mockHistory.push).toHaveBeenCalledWith('/nodes');
});
it('handles failed nodes registration', () => {
@ -159,9 +158,8 @@ describe('nodesRegistrationFinished', () => {
];
spyOn(RegisterNodesActions, 'nodesRegistrationFailed');
spyOn(browserHistory, 'push');
RegisterNodesActions.nodesRegistrationFinished(messagePayload)(
RegisterNodesActions.nodesRegistrationFinished(messagePayload, mockHistory)(
() => {},
() => {
return {
@ -173,7 +171,7 @@ describe('nodesRegistrationFinished', () => {
mockGetIntl
);
expect(browserHistory.push).toHaveBeenCalledWith('/nodes/register');
expect(mockHistory.push).toHaveBeenCalledWith('/nodes/register');
expect(NodesActions.addNodes).toHaveBeenCalledWith(
normalizedRegisteredNodes
);

View File

@ -0,0 +1,19 @@
/**
* 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.
*/
export default {
push: jest.fn()
};

View File

@ -20,7 +20,6 @@ import yaml from 'js-yaml';
import EnvironmentConfigurationConstants
from '../constants/EnvironmentConfigurationConstants';
import { browserHistory } from 'react-router';
import MistralApiService from '../services/MistralApiService';
import NotificationActions from '../actions/NotificationActions';
import MistralApiErrorHandler from '../services/MistralApiErrorHandler';
@ -42,7 +41,7 @@ const messages = defineMessages({
});
export default {
fetchEnvironmentConfiguration(planName, redirectPath) {
fetchEnvironmentConfiguration(planName, redirect) {
return dispatch => {
dispatch(this.fetchEnvironmentConfigurationPending());
MistralApiService.runAction(MistralConstants.CAPABILITIES_GET, {
@ -60,8 +59,8 @@ export default {
'Error retrieving EnvironmentConfigurationActions.fetchEnvironment',
error.stack || error
);
if (redirectPath) {
browserHistory.push(redirectPath);
if (redirect) {
redirect();
}
dispatch(this.fetchEnvironmentConfigurationFailed());
let errorHandler = new MistralApiErrorHandler(error);
@ -91,7 +90,7 @@ export default {
};
},
updateEnvironmentConfiguration(planName, data, formFields, redirectPath) {
updateEnvironmentConfiguration(planName, data, formFields) {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.updateEnvironmentConfigurationPending());
@ -104,9 +103,6 @@ export default {
response.output
).result.environments.map(env => env.path);
dispatch(this.updateEnvironmentConfigurationSuccess(enabledEnvs));
if (redirectPath) {
browserHistory.push(redirectPath);
}
dispatch(
NotificationActions.notify({
title: formatMessage(messages.envConfigUpdatedNotificationTitle),

View File

@ -14,7 +14,6 @@
* under the License.
*/
import { browserHistory } from 'react-router';
import { Map, fromJS } from 'immutable';
import KeystoneApiErrorHandler from '../services/KeystoneApiErrorHandler';
@ -35,8 +34,6 @@ export default {
response.token.id = tokenId;
cookie.save('keystoneAuthTokenId', tokenId, { path: '/' });
dispatch(this.userAuthSuccess(response.token));
ZaqarWebSocketService.init(getState, dispatch);
browserHistory.push(nextPath);
})
.catch(error => {
logger.error(
@ -45,10 +42,6 @@ export default {
);
let errorHandler = new KeystoneApiErrorHandler(error);
cookie.remove('keystoneAuthTokenId');
browserHistory.push({
pathname: '/login',
query: { nextPath: nextPath }
});
dispatch(this.userAuthFailure(errorHandler.errors));
});
};
@ -64,8 +57,6 @@ export default {
response.token.id = tokenId;
cookie.save('keystoneAuthTokenId', tokenId, { path: '/' });
dispatch(this.userAuthSuccess(response.token));
ZaqarWebSocketService.init(getState, dispatch);
browserHistory.push(nextPath);
})
.catch(error => {
logger.error(
@ -109,7 +100,6 @@ export default {
logoutUser() {
return dispatch => {
browserHistory.push('/login');
cookie.remove('keystoneAuthTokenId');
ZaqarWebSocketService.close();
dispatch(this.logoutUserSuccess());

View File

@ -15,7 +15,6 @@
*/
import { defineMessages } from 'react-intl';
import { browserHistory } from 'react-router';
import { startSubmit, stopSubmit } from 'redux-form';
import NotificationActions from '../actions/NotificationActions';
@ -56,7 +55,7 @@ export default {
};
},
fetchParameters(planName, parentPath) {
fetchParameters(planName, redirect) {
return dispatch => {
dispatch(this.fetchParametersPending());
MistralApiService.runAction(MistralConstants.PARAMETERS_GET, {
@ -78,8 +77,8 @@ export default {
})
.catch(error => {
dispatch(this.fetchParametersFailed());
if (parentPath) {
browserHistory.push(parentPath);
if (redirect) {
redirect();
}
logger.error(
'Error in ParametersActions.fetchParameters',
@ -116,7 +115,7 @@ export default {
};
},
updateParameters(planName, data, inputFieldNames, url) {
updateParameters(planName, data, inputFieldNames, redirect) {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(startSubmit('nodesAssignment'));
@ -137,8 +136,8 @@ export default {
type: 'success'
})
);
if (url) {
browserHistory.push(url);
if (redirect) {
redirect();
}
})
.catch(error => {

View File

@ -20,7 +20,6 @@ import { normalize, arrayOf } from 'normalizr';
import when from 'when';
import CurrentPlanActions from '../actions/CurrentPlanActions';
import { browserHistory } from 'react-router';
import logger from '../services/logger';
import MistralApiService from '../services/MistralApiService';
import MistralApiErrorHandler from '../services/MistralApiErrorHandler';
@ -166,14 +165,14 @@ export default {
};
},
updatePlan(planName, planFiles) {
updatePlan(planName, planFiles, history) {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.updatePlanPending(planName));
this._uploadFilesToContainer(planName, fromJS(planFiles), dispatch)
.then(() => {
dispatch(this.updatePlanSuccess(planName));
browserHistory.push('/plans/list');
history.push('/plans');
dispatch(
NotificationActions.notify({
title: formatMessage(messages.planUpdatedNotificationTitle),
@ -195,14 +194,14 @@ export default {
};
},
updatePlanFromTarball(planName, file) {
updatePlanFromTarball(planName, file, history) {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.updatePlanPending(planName));
SwiftApiService.uploadTarball(planName, file)
.then(response => {
dispatch(this.updatePlanSuccess(planName));
browserHistory.push('/plans/list');
history.push('/plans');
dispatch(
NotificationActions.notify({
title: formatMessage(messages.planUpdatedNotificationTitle),
@ -335,7 +334,7 @@ export default {
};
},
createPlanFinished(payload) {
createPlanFinished(payload, history) {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
if (payload.status === 'SUCCESS') {
@ -351,7 +350,7 @@ export default {
})
);
dispatch(this.fetchPlans());
browserHistory.push('/plans/list');
history.push('/plans');
} else {
dispatch(
this.createPlanFailed([{ title: 'Error', message: payload.message }])
@ -436,11 +435,11 @@ export default {
};
},
deletePlan(planName) {
deletePlan(planName, history) {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
dispatch(this.deletePlanPending(planName));
browserHistory.push('/plans/list');
history.push('/plans');
MistralApiService.runAction(MistralConstants.PLAN_DELETE, {
container: planName
})

View File

@ -14,7 +14,6 @@
* under the License.
*/
import { browserHistory } from 'react-router';
import { defineMessages } from 'react-intl';
import { normalize, arrayOf } from 'normalizr';
import { Map } from 'immutable';
@ -69,7 +68,7 @@ export default {
};
},
startNodesRegistration(nodes, redirectPath) {
startNodesRegistration(nodes) {
return (dispatch, getState) => {
dispatch(this.startNodesRegistrationPending(nodes));
MistralApiService.runWorkflow(
@ -124,7 +123,7 @@ export default {
};
},
nodesRegistrationFinished(messagePayload) {
nodesRegistrationFinished(messagePayload, history) {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
const registeredNodes =
@ -152,7 +151,7 @@ export default {
})
);
dispatch(this.nodesRegistrationSuccess());
browserHistory.push('/nodes');
history.push('/nodes');
break;
}
case 'FAILED': {
@ -162,7 +161,7 @@ export default {
message: JSON.stringify(messagePayload.message)
}
];
browserHistory.push('/nodes/register');
history.push('/nodes/register');
// TODO(jtomasek): repopulate nodes registration form with failed nodes provided by message
dispatch(this.nodesRegistrationFailed(errors));
break;

View File

@ -19,14 +19,23 @@ import PlansActions from './PlansActions';
import RegisterNodesActions from './RegisterNodesActions';
import ValidationsActions from './ValidationsActions';
import MistralConstants from '../constants/MistralConstants';
import ZaqarWebSocketService from '../services/ZaqarWebSocketService';
export default {
messageReceived(message) {
initializeConnection(history) {
return (dispatch, getState) => {
ZaqarWebSocketService.init(getState, dispatch, history);
};
},
messageReceived(message, history) {
return (dispatch, getState) => {
const { type, payload } = message.body;
switch (type) {
case MistralConstants.BAREMETAL_REGISTER_OR_UPDATE:
dispatch(RegisterNodesActions.nodesRegistrationFinished(payload));
dispatch(
RegisterNodesActions.nodesRegistrationFinished(payload, history)
);
break;
case MistralConstants.BAREMETAL_INTROSPECT:
@ -43,7 +52,7 @@ export default {
}
case MistralConstants.PLAN_CREATE: {
dispatch(PlansActions.createPlanFinished(payload));
dispatch(PlansActions.createPlanFinished(payload, history));
break;
}

View File

@ -14,18 +14,19 @@
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Login from './Login';
import UserAuthenticator from './UserAuthenticator';
export default class App extends React.Component {
render() {
return (
<div>
{this.props.children}
</div>
<Switch>
<Route path="/login" component={Login} />
<Route path="/" component={UserAuthenticator} />
</Switch>
);
}
}
App.propTypes = {
children: PropTypes.element
};

View File

@ -19,13 +19,18 @@ import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
import DeploymentPlan from './deployment_plan/DeploymentPlan';
import Loader from './ui/Loader';
import LoginActions from '../actions/LoginActions';
import PlansActions from '../actions/PlansActions';
import NavBar from './NavBar';
import Nodes from './nodes/Nodes';
import Plans from './plan/Plans.js';
import PlansActions from '../actions/PlansActions';
import ValidationsList from './validations/ValidationsList';
import WorkflowExecutionsActions from '../actions/WorkflowExecutionsActions';
import ZaqarActions from '../actions/ZaqarActions';
const messages = defineMessages({
loadingDeployments: {
@ -38,27 +43,37 @@ class AuthenticatedContent extends React.Component {
componentDidMount() {
this.props.fetchPlans();
this.props.fetchWorkflowExecutions();
this.props.initializeZaqarConnection();
}
render() {
const {
currentPlanName,
intl,
logoutUser,
noPlans,
plansLoaded,
user
} = this.props;
return (
<Loader
loaded={
this.props.plansLoaded &&
(!!this.props.currentPlanName || this.props.noPlans)
}
content={this.props.intl.formatMessage(messages.loadingDeployments)}
loaded={plansLoaded && (!!currentPlanName || noPlans)}
content={intl.formatMessage(messages.loadingDeployments)}
global
>
<header>
<NavBar
user={this.props.user}
onLogout={this.props.logoutUser.bind(this)}
/>
<NavBar user={user} onLogout={logoutUser.bind(this)} />
</header>
<div className="wrapper-fixed-body container-fluid">
<div className="row">
<div className="col-sm-12 col-lg-9">{this.props.children}</div>
<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" />
</Switch>
</div>
<ValidationsList />
</div>
</div>
@ -72,6 +87,7 @@ AuthenticatedContent.propTypes = {
dispatch: PropTypes.func,
fetchPlans: PropTypes.func,
fetchWorkflowExecutions: PropTypes.func,
initializeZaqarConnection: PropTypes.func.isRequired,
intl: PropTypes.object,
logoutUser: PropTypes.func.isRequired,
noPlans: PropTypes.bool,
@ -79,24 +95,22 @@ AuthenticatedContent.propTypes = {
user: ImmutablePropTypes.map
};
const mapDispatchToProps = dispatch => {
return {
logoutUser: () => dispatch(LoginActions.logoutUser()),
fetchPlans: () => dispatch(PlansActions.fetchPlans()),
fetchWorkflowExecutions: () =>
dispatch(WorkflowExecutionsActions.fetchWorkflowExecutions())
};
};
const mapDispatchToProps = (dispatch, ownProps) => ({
logoutUser: () => dispatch(LoginActions.logoutUser()),
fetchPlans: () => dispatch(PlansActions.fetchPlans()),
fetchWorkflowExecutions: () =>
dispatch(WorkflowExecutionsActions.fetchWorkflowExecutions()),
initializeZaqarConnection: () =>
dispatch(ZaqarActions.initializeConnection(ownProps.history))
});
const mapStateToProps = state => {
return {
currentPlanName: state.currentPlan.currentPlanName,
noPlans: state.plans.get('all').isEmpty(),
plansLoaded: state.plans.get('plansLoaded'),
user: state.login.getIn(['token', 'user'])
};
};
const mapStateToProps = state => ({
currentPlanName: state.currentPlan.currentPlanName,
noPlans: state.plans.get('all').isEmpty(),
plansLoaded: state.plans.get('plansLoaded'),
user: state.login.getIn(['token', 'user'])
});
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(AuthenticatedContent)
withRouter(connect(mapStateToProps, mapDispatchToProps)(AuthenticatedContent))
);

View File

@ -21,8 +21,10 @@ import Formsy from 'formsy-react';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import { Redirect } from 'react-router-dom';
import FormErrorList from './ui/forms/FormErrorList';
import Loader from './ui/Loader';
import LoginInput from './ui/forms/LoginInput';
import LoginActions from '../actions/LoginActions';
import NotificationsToaster from './notifications/NotificationsToaster';
@ -31,6 +33,10 @@ import LogoSvg from '../../img/logo.svg';
import TripleoOwlSvg from '../../img/tripleo-owl.svg';
const messages = defineMessages({
authenticating: {
id: 'UserAuthenticator.authenticating',
defaultMessage: 'Authenticating...'
},
username: {
id: 'Login.username',
defaultMessage: 'Username'
@ -74,7 +80,9 @@ class Login extends React.Component {
}
componentDidUpdate() {
this.invalidateLoginForm(this.props.formFieldErrors.toJS());
if (!this.props.isAuthenticated || this.props.isAuthenticating) {
this.invalidateLoginForm(this.props.formFieldErrors.toJS());
}
}
componentWillUnmount() {
@ -94,83 +102,93 @@ class Login extends React.Component {
}
handleLogin(formData, resetForm, invalidateForm) {
const nextPath = this.props.location.query.nextPath || '/';
const formFields = Object.keys(this.refs.form.inputs);
this.props.dispatch(
LoginActions.authenticateUser(formData, formFields, nextPath)
);
this.props.dispatch(LoginActions.authenticateUser(formData, formFields));
}
render() {
const { formatMessage } = this.props.intl;
const { from } = this.props.location.state || { from: { pathname: '/' } };
const {
formErrors,
isAuthenticating,
isAuthenticated,
intl: { formatMessage }
} = this.props;
return (
<div>
<span id="badge">
<img src={LogoSvg} alt="TripleO" />
</span>
<div className="container">
<div className="row">
<div className="col-sm-12">
<div id="brand">
<img src={TripleoOwlSvg} alt="TripleO" />
if (!isAuthenticated) {
return (
<div>
<span id="badge">
<img src={LogoSvg} alt="TripleO" />
</span>
<Loader
content={formatMessage(messages.authenticating)}
loaded={!isAuthenticating}
global
/>
<div className="container">
<div className="row">
<div className="col-sm-12">
<div id="brand">
<img src={TripleoOwlSvg} alt="TripleO" />
</div>
</div>
<div className="col-sm-7 col-md-6 col-lg-5 login">
<FormErrorList errors={formErrors.toJS()} />
<Formsy.Form
ref="form"
role="form"
className="form-horizontal"
onSubmit={this.handleLogin.bind(this)}
onValid={this._enableButton.bind(this)}
onInvalid={this._disableButton.bind(this)}
>
<LoginInput
name="username"
placeholder={formatMessage(messages.username)}
title={formatMessage(messages.username)}
validationError={formatMessage(messages.usernameRequired)}
required
autoFocus
/>
<LoginInput
type="password"
name="password"
placeholder={formatMessage(messages.password)}
title={formatMessage(messages.password)}
validationError={formatMessage(messages.passwordRequired)}
required
/>
<div className="form-group">
<div className="col-xs-offset-8 col-xs-4 col-sm-4 col-md-4 submit">
<button
type="submit"
disabled={!this.state.canSubmit || isAuthenticating}
className="btn btn-primary btn-lg"
tabIndex="4"
id="Login__loginButton"
>
<FormattedMessage {...messages.login} />
</button>
</div>
</div>
</Formsy.Form>
</div>
<div className="col-sm-5 col-md-6 col-lg-7 details">
<p>
<strong><FormattedMessage {...messages.welcome} /></strong>
<br />
<FormattedMessage {...messages.description} />
</p>
</div>
</div>
<div className="col-sm-7 col-md-6 col-lg-5 login">
<FormErrorList errors={this.props.formErrors.toJS()} />
<Formsy.Form
ref="form"
role="form"
className="form-horizontal"
onSubmit={this.handleLogin.bind(this)}
onValid={this._enableButton.bind(this)}
onInvalid={this._disableButton.bind(this)}
>
<LoginInput
name="username"
placeholder={formatMessage(messages.username)}
title={formatMessage(messages.username)}
validationError={formatMessage(messages.usernameRequired)}
required
autoFocus
/>
<LoginInput
type="password"
name="password"
placeholder={formatMessage(messages.password)}
title={formatMessage(messages.password)}
validationError={formatMessage(messages.passwordRequired)}
required
/>
<div className="form-group">
<div className="col-xs-offset-8 col-xs-4 col-sm-4 col-md-4 submit">
<button
type="submit"
disabled={
!this.state.canSubmit || this.props.isAuthenticating
}
className="btn btn-primary btn-lg"
tabIndex="4"
id="Login__loginButton"
>
<FormattedMessage {...messages.login} />
</button>
</div>
</div>
</Formsy.Form>
</div>
<div className="col-sm-5 col-md-6 col-lg-7 details">
<p>
<strong><FormattedMessage {...messages.welcome} /></strong>
<br />
<FormattedMessage {...messages.description} />
</p>
</div>
</div>
<NotificationsToaster />
</div>
<NotificationsToaster />
</div>
);
);
} else {
return <Redirect to={from} />;
}
}
}
Login.propTypes = {
@ -178,17 +196,17 @@ Login.propTypes = {
formErrors: ImmutablePropTypes.list.isRequired,
formFieldErrors: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object,
isAuthenticated: PropTypes.bool.isRequired,
isAuthenticating: PropTypes.bool.isRequired,
location: PropTypes.object,
userLoggedIn: PropTypes.bool.isRequired
location: PropTypes.object
};
function mapStateToProps(state) {
return {
formErrors: state.login.getIn(['loginForm', 'formErrors']),
formFieldErrors: state.login.getIn(['loginForm', 'formFieldErrors']),
userLoggedIn: state.login.hasIn(['keystoneAccess', 'user']),
isAuthenticating: state.login.get('isAuthenticating')
isAuthenticated: state.login.isAuthenticated,
isAuthenticating: state.login.isAuthenticating
};
}

View File

@ -17,7 +17,7 @@
import { defineMessages, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { getAppConfig } from '../services/utils';

View File

@ -15,11 +15,15 @@
*/
import { connect } from 'react-redux';
import cookie from 'react-cookie';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect } from 'react-router-dom';
import AuthenticatedContent from './AuthenticatedContent';
import Loader from './ui/Loader';
import LoginActions from '../actions/LoginActions';
import NotificationsToaster from './notifications/NotificationsToaster';
const messages = defineMessages({
@ -30,37 +34,71 @@ const messages = defineMessages({
});
/**
* Takes care of authenticating user. User Authentication is triggered in routes
* 'onEnter' to this component. After authentication is resolved, component children
* are rendered. No Actions calling API services can be dispatched from this component
* Takes care of authenticating user. After authentication is resolved, AuthenticatedContent
* is rendered. No Actions calling API services except Keystone can be dispatched from this
* component
*/
class UserAuthenticator extends React.Component {
componentWillMount() {
this.checkAuth(this.props);
}
componentWillReceiveProps(nextProps) {
this.checkAuth(nextProps);
}
checkAuth(props) {
const { isAuthenticated, isAuthenticating } = props;
const keystoneAuthTokenId = cookie.load('keystoneAuthTokenId');
if (!isAuthenticated && !isAuthenticating && keystoneAuthTokenId) {
this.props.authenticateUserViaToken(keystoneAuthTokenId);
}
}
render() {
return (
<div>
<Loader
loaded={this.props.isAuthenticated}
content={this.props.intl.formatMessage(messages.authenticating)}
global
>
{this.props.children}
</Loader>
<NotificationsToaster />
</div>
);
const { isAuthenticating, isAuthenticated, location } = this.props;
const keystoneAuthTokenId = cookie.load('keystoneAuthTokenId');
if (isAuthenticated || isAuthenticating || keystoneAuthTokenId) {
return (
<div>
<Loader
loaded={this.props.isAuthenticated}
content={this.props.intl.formatMessage(messages.authenticating)}
global
>
<AuthenticatedContent />
</Loader>
<NotificationsToaster />
</div>
);
} else {
return (
<Redirect to={{ pathname: '/login', state: { from: location } }} />
);
}
}
}
UserAuthenticator.propTypes = {
children: PropTypes.node,
dispatch: PropTypes.func,
authenticateUserViaToken: PropTypes.func.isRequired,
intl: PropTypes.object,
isAuthenticated: PropTypes.bool.isRequired
isAuthenticated: PropTypes.bool.isRequired,
isAuthenticating: PropTypes.bool.isRequired,
location: PropTypes.object.isRequired
};
const mapStateToProps = state => {
return {
isAuthenticated: state.login.isAuthenticated
isAuthenticated: state.login.isAuthenticated,
isAuthenticating: state.login.isAuthenticating
};
};
export default injectIntl(connect(mapStateToProps)(UserAuthenticator));
const mapDispatchToProps = dispatch => ({
authenticateUserViaToken: tokenId =>
dispatch(LoginActions.authenticateUserViaToken(tokenId))
});
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(UserAuthenticator)
);

View File

@ -17,7 +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';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';

View File

@ -15,7 +15,7 @@
*/
import { defineMessages, FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';

View File

@ -15,12 +15,16 @@
*/
import { defineMessages, FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Link, Redirect, Route, Switch } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
import { checkRunningDeployment } from '../utils/checkRunningDeploymentHOC';
import EnvironmentConfiguration
from '../environment_configuration/EnvironmentConfiguration';
import NavTab from '../ui/NavTab';
import Modal from '../ui/Modal';
import Parameters from '../parameters/Parameters';
const messages = defineMessages({
deploymentConfiguration: {
@ -37,12 +41,13 @@ const messages = defineMessages({
}
});
export default class DeploymentConfiguration extends React.Component {
class DeploymentConfiguration extends React.Component {
render() {
const { location } = this.props;
return (
<Modal dialogClasses="modal-xl">
<div className="modal-header">
<Link to={this.props.parentPath} type="button" className="close">
<Link to="/deployment-plan" type="button" className="close">
<span aria-hidden="true" className="pficon pficon-close" />
</Link>
<h4 className="modal-title">
@ -51,24 +56,42 @@ export default class DeploymentConfiguration extends React.Component {
</div>
<ul className="nav nav-tabs">
<NavTab to="/deployment-plan/configuration/environment">
<NavTab
location={location}
to="/deployment-plan/configuration/environment"
>
<FormattedMessage {...messages.overallSettings} />
</NavTab>
<NavTab to="/deployment-plan/configuration/parameters">
<NavTab
location={location}
to="/deployment-plan/configuration/parameters"
>
<FormattedMessage {...messages.parameters} />
</NavTab>
</ul>
{React.cloneElement(this.props.children, {
currentPlanName: this.props.currentPlanName,
parentPath: this.props.parentPath
})}
<Switch location={location}>
<Route
location={location}
path="/deployment-plan/configuration/environment"
component={EnvironmentConfiguration}
/>
<Route
location={location}
path="/deployment-plan/configuration/parameters"
component={Parameters}
/>
<Redirect
from="/deployment-plan/configuration"
to="/deployment-plan/configuration/environment"
/>
</Switch>
</Modal>
);
}
}
DeploymentConfiguration.propTypes = {
children: PropTypes.node,
currentPlanName: PropTypes.string,
parentPath: PropTypes.string
location: PropTypes.object.isRequired
};
export default checkRunningDeployment(DeploymentConfiguration);

View File

@ -18,7 +18,7 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import DeleteStackButton from './DeleteStackButton';
import InlineNotification from '../ui/InlineNotification';

View File

@ -19,7 +19,10 @@ 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 DeploymentConfiguration from './DeploymentConfiguration';
import DeploymentDetail from '../deployment/DeploymentDetail';
import { getAllPlansButCurrent } from '../../selectors/plans';
import {
getCurrentStack,
@ -51,10 +54,11 @@ import NoPlans from './NoPlans';
import NotificationActions from '../../actions/NotificationActions';
import ParametersActions from '../../actions/ParametersActions';
import PlansActions from '../../actions/PlansActions';
import StacksActions from '../../actions/StacksActions';
import stackStates from '../../constants/StacksConstants';
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({
@ -169,19 +173,10 @@ class DeploymentPlan extends React.Component {
render() {
const { formatMessage } = this.props.intl;
let children;
const currentPlanName = this.props.hasPlans
? this.props.currentPlan.name
: undefined;
// Render children only when current plan is already selected
if (this.props.children && currentPlanName) {
children = React.cloneElement(this.props.children, {
currentPlanName: currentPlanName,
parentPath: '/' + this.props.route.path
});
}
return (
<div className="row">
{this.props.hasPlans
@ -272,7 +267,18 @@ class DeploymentPlan extends React.Component {
: <div className="col-sm-12">
<NoPlans />
</div>}
{children}
<Route
path="/deployment-plan/configuration"
component={DeploymentConfiguration}
/>
<Route
path="/deployment-plan/roles/:roleIdentifier"
component={RoleDetail}
/>
<Route
path="/deployment-plan/deployment-detail"
component={DeploymentDetail}
/>
</div>
);
}
@ -281,7 +287,6 @@ class DeploymentPlan extends React.Component {
DeploymentPlan.propTypes = {
availableNodes: ImmutablePropTypes.map,
availableNodesCountsByRole: ImmutablePropTypes.map.isRequired,
children: PropTypes.node,
choosePlan: PropTypes.func,
currentPlan: ImmutablePropTypes.record,
currentStack: ImmutablePropTypes.record,

View File

@ -16,7 +16,7 @@
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';

View File

@ -15,7 +15,7 @@
*/
import { defineMessages, FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import React from 'react';
const messages = defineMessages({

View File

@ -15,7 +15,7 @@
*/
import { defineMessages, FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import React from 'react';
const messages = defineMessages({
@ -43,11 +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"
query={{ tab: 'newPlan' }}
className="btn btn-lg btn-primary"
>
<Link to="/plans/new" className="btn btn-lg btn-primary">
<span className="fa fa-plus" />
{' '}
<FormattedMessage {...messages.createNewPlan} />

View File

@ -16,7 +16,7 @@
import { defineMessages, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
@ -52,7 +52,7 @@ export default class PlansDropdown extends React.Component {
render() {
if (this.props.plans.isEmpty()) {
return (
<Link className="btn btn-link" to="/plans/list">
<Link className="btn btn-link" to="/plans">
<FormattedMessage {...messages.manageDeployments} />
</Link>
);
@ -64,7 +64,7 @@ export default class PlansDropdown extends React.Component {
</DropdownButton>
{this.renderRecentPlans()}
<DropdownItem key="divider" divider />
<DropdownItem key="plansLink" to="/plans/list">
<DropdownItem key="plansLink" to="/plans">
<FormattedMessage {...messages.manageDeployments} />
</DropdownItem>
</Dropdown>

View File

@ -19,7 +19,7 @@ import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import Formsy from 'formsy-react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link, browserHistory } from 'react-router';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
@ -58,9 +58,8 @@ class EnvironmentConfiguration extends React.Component {
}
componentDidMount() {
this.props.fetchEnvironmentConfiguration(
this.props.currentPlanName,
this.props.parentPath
this.props.fetchEnvironmentConfiguration(this.props.currentPlanName, () =>
this.props.history.push('/deployment-plan')
);
}
@ -111,7 +110,7 @@ class EnvironmentConfiguration extends React.Component {
closeOnSubmit: false
});
browserHistory.push(this.props.parentPath);
this.props.history.push('/deployment-plan');
}
}
@ -210,11 +209,7 @@ class EnvironmentConfiguration extends React.Component {
>
<FormattedMessage {...messages.saveAndClose} />
</button>
<Link
to={this.props.parentPath}
type="button"
className="btn btn-default"
>
<Link to="/deployment-plan" type="button" className="btn btn-default">
<FormattedMessage {...messages.cancel} />
</Link>
</div>
@ -229,16 +224,12 @@ EnvironmentConfiguration.propTypes = {
fetchEnvironmentConfiguration: PropTypes.func,
formErrors: ImmutablePropTypes.list.isRequired,
formFieldErrors: ImmutablePropTypes.map.isRequired,
history: PropTypes.object.isRequired,
isFetching: PropTypes.bool,
location: PropTypes.object,
parentPath: PropTypes.string.isRequired,
updateEnvironmentConfiguration: PropTypes.func
};
EnvironmentConfiguration.defaultProps = {
parentPath: '/deployment-plan'
};
function mapStateToProps(state) {
return {
currentPlanName: state.currentPlan.currentPlanName,
@ -259,18 +250,12 @@ function mapDispatchToProps(dispatch) {
EnvironmentConfigurationActions.fetchEnvironmentConfiguration(planName)
);
},
updateEnvironmentConfiguration: (
planName,
data,
inputFields,
parentPath
) => {
updateEnvironmentConfiguration: (planName, data, inputFields) => {
dispatch(
EnvironmentConfigurationActions.updateEnvironmentConfiguration(
planName,
data,
inputFields,
parentPath
inputFields
)
);
}

View File

@ -17,9 +17,10 @@
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
import { Route } from 'react-router-dom';
import { getFilterByName } from '../../selectors/filters';
import { getFilteredNodes, nodesInProgress } from '../../selectors/nodes';
@ -29,6 +30,7 @@ import NodesListForm from './NodesListView/NodesListForm';
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({
@ -108,13 +110,12 @@ class Nodes extends React.Component {
<NodesToolbar />
{this.renderContentView()}
</Loader>
{this.props.children}
<Route path="/nodes/register" component={RegisterNodesDialog} />
</div>
);
}
}
Nodes.propTypes = {
children: PropTypes.node,
contentView: PropTypes.string.isRequired,
currentPlanName: PropTypes.string.isRequired,
fetchNodes: PropTypes.func.isRequired,

View File

@ -18,7 +18,7 @@ import { connect } from 'react-redux';
import { fromJS } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ClassNames from 'classnames';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';

View File

@ -19,7 +19,7 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import Formsy from 'formsy-react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { isObjectLike, mapValues } from 'lodash';
import { Link, browserHistory } from 'react-router';
import { Link } from 'react-router-dom';
import { fromJS, is } from 'immutable';
import PropTypes from 'prop-types';
import React from 'react';
@ -68,11 +68,10 @@ class Parameters extends React.Component {
currentPlanName,
fetchEnvironmentConfiguration,
fetchParameters,
isFetchingParameters,
parentPath
isFetchingParameters
} = this.props;
fetchEnvironmentConfiguration(currentPlanName, parentPath);
!isFetchingParameters && fetchParameters(currentPlanName, parentPath);
fetchEnvironmentConfiguration(currentPlanName);
!isFetchingParameters && fetchParameters(currentPlanName);
}
componentDidUpdate() {
@ -135,7 +134,7 @@ class Parameters extends React.Component {
closeOnSubmit: false
});
browserHistory.push(this.props.parentPath);
this.props.history.push('/deployment-plan');
}
}
@ -281,14 +280,9 @@ Parameters.propTypes = {
mistralParameters: ImmutablePropTypes.map.isRequired,
parameters: ImmutablePropTypes.map.isRequired,
parametersLoaded: PropTypes.bool,
parentPath: PropTypes.string.isRequired,
updateParameters: PropTypes.func
};
Parameters.defaultProps = {
parentPath: '/deployment-plan'
};
function mapStateToProps(state, ownProps) {
return {
allParameters: state.parameters.parameters,
@ -303,28 +297,30 @@ function mapStateToProps(state, ownProps) {
};
}
function mapDispatchToProps(dispatch) {
function mapDispatchToProps(dispatch, ownProps) {
return {
fetchEnvironmentConfiguration: (currentPlanName, redirectPath) => {
fetchEnvironmentConfiguration: currentPlanName => {
dispatch(
EnvironmentConfigurationActions.fetchEnvironmentConfiguration(
currentPlanName,
redirectPath
() => ownProps.history.push('/deployment-plan')
)
);
},
fetchParameters: (currentPlanName, redirectPath) => {
fetchParameters: currentPlanName => {
dispatch(
ParametersActions.fetchParameters(currentPlanName, redirectPath)
ParametersActions.fetchParameters(currentPlanName, () =>
ownProps.history.push('/deployment-plan')
)
);
},
updateParameters: (currentPlanName, data, inputFields, redirectPath) => {
updateParameters: (currentPlanName, data, inputFields, redirect) => {
dispatch(
ParametersActions.updateParameters(
currentPlanName,
data,
inputFields,
redirectPath
redirect
)
);
}

View File

@ -18,7 +18,7 @@ import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import PlansActions from '../../actions/PlansActions';
import Modal from '../ui/Modal';
@ -44,7 +44,7 @@ const messages = defineMessages({
class DeletePlan extends React.Component {
getNameFromUrl() {
let planName = this.props.params.planName || '';
let planName = this.props.match.params.planName || '';
return planName.replace(/[^A-Za-z0-9_-]*/g, '');
}
@ -59,7 +59,7 @@ class DeletePlan extends React.Component {
return (
<Modal dialogClasses="modal-sm" id="DeletePlan__deletePlanModal">
<div className="modal-header">
<Link to="/plans/list" type="button" className="close">
<Link to="/plans" 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/list"
to="/plans"
type="button"
className="btn btn-default"
id="DeletePlan__cancelDeletePlanModalButton"
@ -112,13 +112,14 @@ class DeletePlan extends React.Component {
DeletePlan.propTypes = {
deletePlan: PropTypes.func,
match: PropTypes.object,
params: PropTypes.object
};
function mapDispatchToProps(dispatch) {
function mapDispatchToProps(dispatch, ownProps) {
return {
deletePlan: planName => {
dispatch(PlansActions.deletePlan(planName));
dispatch(PlansActions.deletePlan(planName, ownProps.history));
}
};
}

View File

@ -18,7 +18,7 @@ import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import Formsy from 'formsy-react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
@ -96,7 +96,7 @@ class EditPlan extends React.Component {
}
getNameFromUrl() {
let planName = this.props.params.planName || '';
let planName = this.props.match.params.planName || '';
return planName.replace(/[^A-Za-z0-9_-]*/g, '');
}
@ -116,7 +116,7 @@ class EditPlan extends React.Component {
onInvalid={this.onFormInvalid.bind(this)}
>
<div className="modal-header">
<Link to="/plans/list" type="button" className="close">
<Link to="/plans" type="button" className="close">
<span aria-hidden="true" className="pficon pficon-close" />
</Link>
<h4>
@ -135,7 +135,6 @@ class EditPlan extends React.Component {
<ModalFormErrorList errors={this.props.planFormErrors.toJS()} />
<div className="modal-body">
<PlanEditFormTabs
currentTab={this.props.location.query.tab || 'editPlan'}
selectedFiles={this.state.selectedFiles}
planName={this.getNameFromUrl()}
planFiles={planFiles}
@ -152,7 +151,7 @@ class EditPlan extends React.Component {
>
<FormattedMessage {...messages.uploadAndUpdate} />
</button>
<Link to="/plans/list" type="button" className="btn btn-default">
<Link to="/plans" type="button" className="btn btn-default">
<FormattedMessage {...messages.cancel} />
</Link>
</div>
@ -167,7 +166,7 @@ EditPlan.propTypes = {
history: PropTypes.object,
intl: PropTypes.object,
isTransitioningPlan: PropTypes.bool,
location: PropTypes.object,
match: PropTypes.object,
params: PropTypes.object,
planFormErrors: ImmutablePropTypes.list,
plans: ImmutablePropTypes.map,
@ -183,16 +182,18 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
function mapDispatchToProps(dispatch, ownProps) {
return {
fetchPlan: planName => {
dispatch(PlansActions.fetchPlan(planName));
},
updatePlan: (planName, files) => {
dispatch(PlansActions.updatePlan(planName, files));
dispatch(PlansActions.updatePlan(planName, files, ownProps.history));
},
updatePlanFromTarball: (planName, files) => {
dispatch(PlansActions.updatePlanFromTarball(planName, files));
dispatch(
PlansActions.updatePlanFromTarball(planName, files, ownProps.history)
);
}
};
}

View File

@ -18,7 +18,7 @@ import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import PlansActions from '../../actions/PlansActions';
import Loader from '../ui/Loader';
@ -60,7 +60,7 @@ class ExportPlan extends React.Component {
}
getNameFromUrl() {
let planName = this.props.params.planName || '';
let planName = this.props.match.params.planName || '';
return planName.replace(/[^A-Za-z0-9_-]*/g, '');
}
@ -68,7 +68,7 @@ class ExportPlan extends React.Component {
return (
<Modal dialogClasses="modal-sm" id="ExportPlan__exportPlanModal">
<div className="modal-header">
<Link to="/plans/list" type="button" className="close">
<Link to="/plans" 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/list"
to="/plans"
type="button"
className="btn btn-default"
id="ExportPlan__cancelExportPlanModalButton"
@ -124,6 +124,7 @@ ExportPlan.propTypes = {
exportPlan: PropTypes.func,
intl: PropTypes.object,
isExportingPlan: PropTypes.bool,
match: PropTypes.object,
params: PropTypes.object,
planExportUrl: PropTypes.string
};

View File

@ -16,7 +16,7 @@
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
@ -88,11 +88,7 @@ class ListPlans extends React.Component {
<FormattedMessage {...messages.noPlans} />
</p>
<p className="text-center">
<Link
to="/plans/new"
query={{ tab: 'newPlan' }}
className="btn btn-success"
>
<Link to="/plans/new" className="btn btn-success">
<FormattedMessage {...messages.createNewPlan} />
</Link>
</p>
@ -105,7 +101,6 @@ class ListPlans extends React.Component {
return (
<Link
to="/plans/new"
query={{ tab: 'newPlan' }}
className="btn btn-primary"
id="ListPlans__newPlanLink"
>
@ -206,7 +201,6 @@ class RowActionsCell extends React.Component {
<Link
key="edit"
to={`/plans/${plan.name}/edit`}
query={{ tab: 'editPlan' }}
className="btn btn-xs btn-default"
>
<FormattedMessage {...messages.edit} />

View File

@ -18,7 +18,7 @@ import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import Formsy from 'formsy-react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
@ -107,7 +107,7 @@ class NewPlan extends React.Component {
>
<div className="modal-header">
<Link
to="/plans/list"
to="/plans"
type="button"
onClick={() => this.props.cancelCreatePlan()}
className="close"
@ -126,7 +126,6 @@ class NewPlan extends React.Component {
<ModalFormErrorList errors={this.props.planFormErrors.toJS()} />
<div className="modal-body">
<PlanFormTabs
currentTab={this.props.location.query.tab || 'newPlan'}
selectedFiles={this.state.selectedFiles}
setUploadType={this.setUploadType.bind(this)}
uploadType={this.state.uploadType}
@ -143,7 +142,7 @@ class NewPlan extends React.Component {
<FormattedMessage {...messages.uploadAndCreate} />
</button>
<Link
to="/plans/list"
to="/plans"
type="button"
onClick={() => this.props.cancelCreatePlan()}
className="btn btn-default"

View File

@ -14,6 +14,7 @@
* under the License.
*/
import ClassNames from 'classnames';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { List } from 'immutable';
@ -21,7 +22,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import HorizontalStaticText from '../ui/forms/HorizontalStaticText';
import NavTab from '../ui/NavTab';
import Tab from '../ui/Tab';
import PlanFileInput from './PlanFileInput';
import PlanFilesTab from './PlanFilesTab';
import PlanUploadTypeRadios from './PlanUploadTypeRadios';
@ -50,8 +51,19 @@ const messages = defineMessages({
});
export default class PlanEditFormTabs extends React.Component {
constructor() {
super();
this.state = {
activeTab: 'editPlan'
};
}
setActiveTab(tabName) {
return this.props.currentTab === tabName ? 'active' : '';
this.setState({ activeTab: tabName });
}
isActiveTab(tabName) {
return this.state.activeTab === tabName;
}
getFileCount() {
@ -66,30 +78,28 @@ export default class PlanEditFormTabs extends React.Component {
return (
<div>
<ul className="nav nav-tabs">
<NavTab
to={`/plans/${this.props.planName}/edit`}
query={{ tab: 'editPlan' }}
>
<FormattedMessage {...messages.updatePlan} />
</NavTab>
<NavTab
to={`/plans/${this.props.planName}/edit`}
query={{ tab: 'planFiles' }}
>
<FormattedMessage {...messages.files} /> <span className="badge">
{this.getFileCount.bind(this)()}
</span>
</NavTab>
<Tab isActive={this.isActiveTab('editPlan')}>
<a className="link" onClick={() => this.setActiveTab('editPlan')}>
<FormattedMessage {...messages.updatePlan} />
</a>
</Tab>
<Tab isActive={this.isActiveTab('planFiles')}>
<a className="link" onClick={() => this.setActiveTab('planFiles')}>
<FormattedMessage {...messages.files} /> <span className="badge">
{this.getFileCount.bind(this)()}
</span>
</a>
</Tab>
</ul>
<div className="tab-content">
<PlanFormTab
active={this.setActiveTab('editPlan')}
active={this.isActiveTab('editPlan')}
planName={this.props.planName}
uploadType={this.props.uploadType}
setUploadType={this.props.setUploadType}
/>
<PlanFilesTab
active={this.setActiveTab('planFiles')}
active={this.isActiveTab('planFiles')}
planFiles={this.props.planFiles}
selectedFiles={this.props.selectedFiles}
/>
@ -99,7 +109,6 @@ export default class PlanEditFormTabs extends React.Component {
}
}
PlanEditFormTabs.propTypes = {
currentTab: PropTypes.string,
planFiles: ImmutablePropTypes.map,
planName: PropTypes.string,
selectedFiles: PropTypes.array,
@ -114,7 +123,9 @@ class _PlanFormTab extends React.Component {
render() {
const { formatMessage } = this.props.intl;
return (
<div className={`tab-pane ${this.props.active}`}>
<div
className={ClassNames({ 'tab-pane': true, active: this.props.active })}
>
<HorizontalStaticText
title={formatMessage(messages.planName)}
text={this.props.planName}
@ -142,10 +153,12 @@ class _PlanFormTab extends React.Component {
}
}
_PlanFormTab.propTypes = {
active: PropTypes.string,
active: PropTypes.bool.isRequired,
intl: PropTypes.object,
planName: PropTypes.string,
setUploadType: PropTypes.func.isRequired,
uploadType: PropTypes.string.isRequired
};
_PlanFormTab.defaultProps = { active: false };
const PlanFormTab = injectIntl(_PlanFormTab);

View File

@ -14,26 +14,25 @@
* under the License.
*/
import ClassNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
import FileList from './FileList';
export default class PlanFilesTab extends React.Component {
render() {
return (
<div className={`tab-pane ${this.props.active}`}>
<FileList
planFiles={this.props.planFiles}
selectedFiles={this.props.selectedFiles}
/>
</div>
);
}
}
const PlanFilesTab = ({ active, ...rest }) => (
<div className={ClassNames({ 'tab-pane': true, active: active })}>
<FileList {...rest} />
</div>
);
PlanFilesTab.propTypes = {
active: PropTypes.string,
active: PropTypes.bool.isRequired,
planFiles: ImmutablePropTypes.map,
selectedFiles: PropTypes.array
};
PlanFilesTab.defaultProps = {
active: false
};
export default PlanFilesTab;

View File

@ -14,12 +14,13 @@
* under the License.
*/
import ClassNames from 'classnames';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import HorizontalInput from '../ui/forms/HorizontalInput';
import NavTab from '../ui/NavTab';
import Tab from '../ui/Tab';
import PlanFileInput from './PlanFileInput';
import PlanFilesTab from './PlanFilesTab';
import PlanUploadTypeRadios from './PlanUploadTypeRadios';
@ -57,31 +58,46 @@ const messages = defineMessages({
});
export default class PlanFormTabs extends React.Component {
constructor() {
super();
this.state = {
activeTab: 'newPlan'
};
}
setActiveTab(tabName) {
return this.props.currentTab === tabName ? 'active' : '';
this.setState({ activeTab: tabName });
}
isActiveTab(tabName) {
return this.state.activeTab === tabName;
}
render() {
return (
<div>
<ul className="nav nav-tabs">
<NavTab to="/plans/new" query={{ tab: 'newPlan' }}>
<FormattedMessage {...messages.newPlan} />
</NavTab>
<NavTab to="/plans/new" query={{ tab: 'planFiles' }}>
<FormattedMessage {...messages.files} /> <span className="badge">
{this.props.selectedFiles.length}
</span>
</NavTab>
<Tab isActive={this.isActiveTab('newPlan')}>
<a className="link" onClick={() => this.setActiveTab('newPlan')}>
<FormattedMessage {...messages.newPlan} />
</a>
</Tab>
<Tab isActive={this.isActiveTab('planFiles')}>
<a className="link" onClick={() => this.setActiveTab('planFiles')}>
<FormattedMessage {...messages.files} /> <span className="badge">
{this.props.selectedFiles.length}
</span>
</a>
</Tab>
</ul>
<div className="tab-content">
<PlanFormTab
active={this.setActiveTab('newPlan')}
active={this.isActiveTab('newPlan')}
uploadType={this.props.uploadType}
setUploadType={this.props.setUploadType}
/>
<PlanFilesTab
active={this.setActiveTab('planFiles')}
active={this.isActiveTab('planFiles')}
selectedFiles={this.props.selectedFiles}
/>
</div>
@ -90,7 +106,6 @@ export default class PlanFormTabs extends React.Component {
}
}
PlanFormTabs.propTypes = {
currentTab: PropTypes.string,
selectedFiles: PropTypes.array,
setUploadType: PropTypes.func.isRequired,
uploadType: PropTypes.string.isRequired
@ -104,7 +119,9 @@ class _PlanFormTab extends React.Component {
render() {
const { formatMessage } = this.props.intl;
return (
<div className={`tab-pane ${this.props.active}`}>
<div
className={ClassNames({ 'tab-pane': true, active: this.props.active })}
>
<HorizontalInput
name="planName"
title={formatMessage(messages.planName)}
@ -136,10 +153,11 @@ class _PlanFormTab extends React.Component {
}
}
_PlanFormTab.propTypes = {
active: PropTypes.string,
active: PropTypes.bool.isRequired,
intl: PropTypes.object,
setUploadType: PropTypes.func.isRequired,
uploadType: PropTypes.string.isRequired
};
_PlanFormTab.defaultProps = { active: false };
const PlanFormTab = injectIntl(_PlanFormTab);

View File

@ -14,21 +14,27 @@
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { Route } from 'react-router-dom';
import ListPlans from './ListPlans';
import DeletePlan from './DeletePlan';
import EditPlan from './EditPlan';
import ExportPlan from './ExportPlan';
import NewPlan from './NewPlan';
export default class Plans extends React.Component {
render() {
return (
<div className="row">
<div className="col-sm-12">
{this.props.children}
<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} />
</div>
</div>
);
}
}
Plans.propTypes = {
children: PropTypes.node
};

View File

@ -20,10 +20,11 @@ import Formsy from 'formsy-react';
import { fromJS, is } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { isObjectLike, mapValues } from 'lodash';
import { Link, Redirect, Route, Switch } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
import { Link } from 'react-router';
import { checkRunningDeployment } from '../utils/checkRunningDeploymentHOC';
import { getRole } from '../../selectors/roles';
import { getRoleServices } from '../../selectors/parameters';
import Loader from '../ui/Loader';
@ -37,6 +38,9 @@ import {
} from '../ui/ModalPanel';
import NavTab from '../ui/NavTab';
import ParametersActions from '../../actions/ParametersActions';
import RoleNetworkConfig from './RoleNetworkConfig';
import RoleParameters from './RoleParameters';
import RoleServices from './RoleServices';
const messages = defineMessages({
networkConfiguration: {
@ -134,30 +138,40 @@ class RoleDetail extends React.Component {
}
renderRoleTabs() {
if (this.props.rolesLoaded && this.props.parametersLoaded) {
const {
formErrors,
location,
match: { params: urlParams },
parametersLoaded,
rolesLoaded
} = this.props;
if (rolesLoaded && parametersLoaded) {
return (
<div className="row">
<ul className="nav nav-tabs">
<NavTab
to={`/deployment-plan/roles/${this.props.params.roleIdentifier}/parameters`}
location={location}
to={`/deployment-plan/roles/${urlParams.roleIdentifier}/parameters`}
>
<FormattedMessage {...messages.parameters} />
</NavTab>
<NavTab
to={`/deployment-plan/roles/${this.props.params.roleIdentifier}/services`}
location={location}
to={`/deployment-plan/roles/${urlParams.roleIdentifier}/services`}
>
<FormattedMessage {...messages.services} />
</NavTab>
<NavTab
location={location}
to={
`/deployment-plan/roles/${this.props.params.roleIdentifier}` +
`/deployment-plan/roles/${urlParams.roleIdentifier}` +
'/network-configuration'
}
>
<FormattedMessage {...messages.networkConfiguration} />
</NavTab>
</ul>
<ModalFormErrorList errors={this.props.formErrors.toJS()} />
<ModalFormErrorList errors={formErrors.toJS()} />
</div>
);
}
@ -166,6 +180,7 @@ 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;
return (
<Formsy.Form
@ -198,7 +213,24 @@ class RoleDetail extends React.Component {
)}
loaded={dataLoaded}
>
{this.props.children}
<Switch>
<Route
path="/deployment-plan/roles/:roleIdentifier/parameters"
component={RoleParameters}
/>
<Route
path="/deployment-plan/roles/:roleIdentifier/services"
component={RoleServices}
/>
<Route
path="/deployment-plan/roles/:roleIdentifier/network-configuration"
component={RoleNetworkConfig}
/>
<Redirect
from="/deployment-plan/roles/:roleIdentifier"
to={`/deployment-plan/roles/${urlParams.roleIdentifier}/parameters`}
/>
</Switch>
</Loader>
</ModalPanelBody>
{dataLoaded
@ -219,14 +251,14 @@ class RoleDetail extends React.Component {
}
RoleDetail.propTypes = {
allParameters: ImmutablePropTypes.map.isRequired,
children: PropTypes.node,
currentPlanName: PropTypes.string,
fetchParameters: PropTypes.func,
formErrors: ImmutablePropTypes.list,
formFieldErrors: ImmutablePropTypes.map,
intl: PropTypes.object,
location: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
parametersLoaded: PropTypes.bool.isRequired,
params: PropTypes.object.isRequired,
role: ImmutablePropTypes.record,
rolesLoaded: PropTypes.bool.isRequired,
updateParameters: PropTypes.func
@ -239,8 +271,8 @@ function mapStateToProps(state, props) {
formFieldErrors: state.parameters.form.get('formFieldErrors'),
allParameters: state.parameters.parameters,
parametersLoaded: state.parameters.loaded,
role: getRole(state, props.params.roleIdentifier),
roleServices: getRoleServices(state, props.params.roleIdentifier),
role: getRole(state, props.match.params.roleIdentifier),
roleServices: getRoleServices(state, props.match.params.roleIdentifier),
rolesLoaded: state.roles.get('loaded')
};
}
@ -265,6 +297,6 @@ function mapDispatchToProps(dispatch) {
};
}
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(RoleDetail)
export default checkRunningDeployment(
injectIntl(connect(mapStateToProps, mapDispatchToProps)(RoleDetail))
);

View File

@ -39,18 +39,17 @@ class RoleNetworkConfig extends React.Component {
RoleNetworkConfig.propTypes = {
description: PropTypes.string,
mistralParameters: ImmutablePropTypes.map.isRequired,
parameters: ImmutablePropTypes.map.isRequired,
params: PropTypes.object.isRequired
parameters: ImmutablePropTypes.map.isRequired
};
function mapStateToProps(state, props) {
return {
description: getRoleNetworkConfig(state, props.params.roleIdentifier)
description: getRoleNetworkConfig(state, props.match.params.roleIdentifier)
.description,
mistralParameters: state.parameters.mistralParameters,
parameters: getRoleNetworkConfig(state, props.params.roleIdentifier)
parameters: getRoleNetworkConfig(state, props.match.params.roleIdentifier)
.parameters,
role: getRole(state, props.params.roleIdentifier)
role: getRole(state, props.match.params.roleIdentifier)
};
}

View File

@ -36,16 +36,16 @@ class RoleParameters extends React.Component {
}
}
RoleParameters.propTypes = {
match: PropTypes.object.isRequired,
mistralParameters: ImmutablePropTypes.map.isRequired,
parameters: ImmutablePropTypes.map.isRequired,
params: PropTypes.object.isRequired
parameters: ImmutablePropTypes.map.isRequired
};
function mapStateToProps(state, props) {
return {
mistralParameters: state.parameters.mistralParameters,
parameters: getRoleParameters(state, props.params.roleIdentifier),
role: getRole(state, props.params.roleIdentifier)
parameters: getRoleParameters(state, props.match.params.roleIdentifier),
role: getRole(state, props.match.params.roleIdentifier)
};
}

View File

@ -99,7 +99,6 @@ class RoleServices extends React.Component {
RoleServices.propTypes = {
intl: PropTypes.object,
mistralParameters: ImmutablePropTypes.map.isRequired,
params: PropTypes.object.isRequired,
role: ImmutablePropTypes.record.isRequired,
services: ImmutablePropTypes.map.isRequired
};
@ -107,8 +106,8 @@ RoleServices.propTypes = {
function mapStateToProps(state, props) {
return {
mistralParameters: state.parameters.mistralParameters,
role: getRole(state, props.params.roleIdentifier),
services: getRoleServices(state, props.params.roleIdentifier)
role: getRole(state, props.match.params.roleIdentifier),
services: getRoleServices(state, props.match.params.roleIdentifier)
};
}

View File

@ -16,7 +16,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Link as ReactRouterLink } from 'react-router';
import { Link as ReactRouterLink } from 'react-router-dom';
export default class Link extends React.Component {
render() {

View File

@ -16,28 +16,32 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Link } from 'react-router';
import { Link, Route } from 'react-router-dom';
export default class NavTab extends Link {
render() {
let router = this.context.router;
let isActive = router.isActive(
{ pathname: this.props.to, query: this.props.query },
this.props.onlyActiveOnIndex
);
let className = isActive ? 'active' : '';
let link = <Link {...this.props} />;
return <li className={className}>{link}</li>;
}
}
const NavTab = ({ activeClassName, children, to, exact, location }) => {
return (
<Route
location={location}
path={typeof to === 'object' ? to.pathname : to}
exact={exact}
children={({ match, location }) => (
<li className={match ? activeClassName : ''}>
<Link to={to}>{children}</Link>
</li>
)}
/>
);
};
NavTab.propTypes = {
onlyActiveOnIndex: PropTypes.bool.isRequired,
query: PropTypes.object,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.route]).isRequired
activeClassName: PropTypes.string.isRequired,
children: PropTypes.node,
exact: PropTypes.bool.isRequired,
location: PropTypes.object,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
};
NavTab.defaultProps = {
onlyActiveOnIndex: false
};
NavTab.contextTypes = {
router: PropTypes.object
activeClassName: 'active',
exact: false
};
export default NavTab;

View File

@ -20,7 +20,7 @@ import React from 'react';
export default class Tab extends React.Component {
render() {
let className = this.props.isActive ? 'active' : '';
return <li {...this.props} className={className}>{this.props.children}</li>;
return <li className={className}>{this.props.children}</li>;
}
}
Tab.propTypes = {

View File

@ -15,7 +15,7 @@
*/
import ClassNames from 'classnames';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';

View File

@ -0,0 +1,64 @@
/**
* 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 PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import React from 'react';
import {
currentPlanNameSelector as getCurrentPlanName
} from '../../selectors/plans';
import { getCurrentStackDeploymentInProgress } from '../../selectors/stacks';
import NotificationActions from '../../actions/NotificationActions';
export const checkRunningDeployment = WrappedComponent => {
class CheckRunningDeploymentHOC extends React.Component {
componentDidMount() {
if (this.props.currentStackDeploymentInProgress) {
this.props.notify({
title: 'Not allowed',
message: `A deployment for the plan ${this.props.currentPlanName} is already in progress.`,
type: 'warning'
});
}
}
render() {
return this.props.currentStackDeploymentInProgress
? <Redirect to="/deployment-plan" />
: <WrappedComponent {...this.props} />;
}
}
CheckRunningDeploymentHOC.propTypes = {
currentPlanName: PropTypes.string,
currentStackDeploymentInProgress: PropTypes.bool.isRequired,
notify: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
currentPlanName: getCurrentPlanName(state),
currentStackDeploymentInProgress: getCurrentStackDeploymentInProgress(state)
});
const mapDispatchToProps = dispatch => ({
notify: notification => dispatch(NotificationActions.notify(notification))
});
return connect(mapStateToProps, mapDispatchToProps)(
CheckRunningDeploymentHOC
);
};

View File

@ -19,145 +19,22 @@ import 'babel-polyfill';
import { Provider } from 'react-redux';
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, Redirect } from 'react-router';
import { browserHistory } from 'react-router';
import cookie from 'react-cookie';
import { BrowserRouter as Router } from 'react-router-dom';
import App from './components/App';
import AuthenticatedContent from './components/AuthenticatedContent';
import UserAuthenticator from './components/UserAuthenticator';
import DeletePlan from './components/plan/DeletePlan';
import DeploymentConfiguration
from './components/deployment_plan/DeploymentConfiguration';
import DeploymentDetail from './components/deployment/DeploymentDetail';
import DeploymentPlan from './components/deployment_plan/DeploymentPlan';
import EditPlan from './components/plan/EditPlan';
import ExportPlan from './components/plan/ExportPlan';
import EnvironmentConfiguration
from './components/environment_configuration/EnvironmentConfiguration.js';
import { getCurrentStackDeploymentInProgress } from './selectors/stacks';
import I18nProvider from './components/i18n/I18nProvider';
import initFormsy from './components/utils/Formsy';
import ListPlans from './components/plan/ListPlans';
import Login from './components/Login';
import LoginActions from './actions/LoginActions';
import NewPlan from './components/plan/NewPlan';
import Nodes from './components/nodes/Nodes';
import NotificationActions from './actions/NotificationActions';
import Parameters from './components/parameters/Parameters.js';
import Plans from './components/plan/Plans.js';
import RegisterNodesDialog from './components/nodes/RegisterNodesDialog';
import RoleDetail from './components/roles/RoleDetail';
import RoleNetworkConfig from './components/roles/RoleNetworkConfig';
import RoleParameters from './components/roles/RoleParameters';
import RoleServices from './components/roles/RoleServices';
import store from './store';
import '../less/base.less';
/**
* @function checkAuth
* If user is not logged in, check if there is an auth token in a cookie
* If there is, try to login with this token, else redirect to Login
*/
function checkAuth(nextState, replace) {
if (!store.getState().login.hasIn(['keystoneAccess', 'user'])) {
const keystoneAuthTokenId = cookie.load('keystoneAuthTokenId');
if (keystoneAuthTokenId) {
const nextPath =
nextState.location.pathname + nextState.location.search || '/';
store.dispatch(
LoginActions.authenticateUserViaToken(keystoneAuthTokenId, nextPath)
);
} else {
replace({
pathname: '/login',
query: {
nextPath: nextState.location.pathname + nextState.location.search
}
});
}
}
}
function checkRunningDeployment(nextState, replace) {
const state = store.getState();
let currentPlanName = state.currentPlan.currentPlanName;
if (getCurrentStackDeploymentInProgress(state)) {
store.dispatch(
NotificationActions.notify({
title: 'Not allowed',
message: `A deployment for the plan ${currentPlanName} is already in progress.`,
type: 'warning'
})
);
// TODO(flfuchs): Redirect to deployment status modal instead of DeploymentPlan
// page (in separate patch).
replace('/deployment-plan/');
}
}
let routes = (
<Route>
<Redirect from="/" to="/deployment-plan" />
<Route path="/" component={App}>
<Route component={UserAuthenticator} onEnter={checkAuth}>
<Route component={AuthenticatedContent}>
<Route path="deployment-plan" component={DeploymentPlan}>
<Redirect from="configuration" to="configuration/environment" />
<Route
path="configuration"
component={DeploymentConfiguration}
onEnter={checkRunningDeployment}
>
<Route path="environment" component={EnvironmentConfiguration} />
<Route path="parameters" component={Parameters} />
</Route>
<Redirect
from="roles/:roleIdentifier"
to="roles/:roleIdentifier/parameters"
/>
<Route
path="roles/:roleIdentifier"
component={RoleDetail}
onEnter={checkRunningDeployment}
>
<Route path="parameters" component={RoleParameters} />
<Route path="services" component={RoleServices} />
<Route
path="network-configuration"
component={RoleNetworkConfig}
/>
</Route>
<Route path="deployment-detail" component={DeploymentDetail} />
</Route>
<Route path="nodes" component={Nodes}>
<Route path="register" component={RegisterNodesDialog} />
</Route>
<Redirect from="plans" to="plans/list" />
<Route path="plans" component={Plans}>
<Route path="list" component={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>
</Route>
</Route>
</Route>
<Route path="login" component={Login} />
</Route>
</Route>
);
initFormsy();
ReactDOM.render(
<Provider store={store}>
<I18nProvider>
<Router history={browserHistory}>{routes}</Router>
<Router>
<App />
</Router>
</I18nProvider>
</Provider>,
document.getElementById('react-app-index')

View File

@ -27,7 +27,7 @@ export default {
socket: null,
clientID: null,
init(getState, dispatch) {
init(getState, dispatch, history) {
when.try(getServiceUrl, 'zaqar-websocket').then(serviceUrl => {
this.socket = new WebSocket(serviceUrl);
this.clientID = uuid.v4();
@ -55,7 +55,7 @@ export default {
};
this.socket.onmessage = evt => {
dispatch(ZaqarActions.messageReceived(JSON.parse(evt.data)));
dispatch(ZaqarActions.messageReceived(JSON.parse(evt.data), history));
};
});
},