Merge "Add container images configuration wizard"

This commit is contained in:
Zuul 2018-12-11 12:01:45 +00:00 committed by Gerrit Code Review
commit 6e16cd4034
13 changed files with 912 additions and 0 deletions

View File

@ -0,0 +1,92 @@
/**
* Copyright 2018 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 { startSubmit, stopSubmit, reset } from 'redux-form';
import { defineMessages } from 'react-intl';
import { CONTAINER_IMAGES_PREPARE_SUCCESS } from '../constants/ContainerImagesConstants';
import MistralConstants from '../constants/MistralConstants';
import { getCurrentPlanName } from '../selectors/plans';
import { startWorkflow } from './WorkflowActions';
import { handleErrors } from './ErrorActions';
const messages = defineMessages({
configureImagesFailed: {
id: 'ContainerImagesActions.configureImagesFailed',
defaultMessage: 'Generating container images configuration failed'
}
});
export const startContainerImagesPrepare = values => (
dispatch,
getState,
{ getIntl }
) => {
const currentPlanName = getCurrentPlanName(getState());
const { formatMessage } = getIntl(getState());
dispatch(startSubmit('containerImagesPrepareForm'));
return dispatch(
startWorkflow(
MistralConstants.CONTAINER_IMAGES_PREPARE_DEFAULT,
{
container: currentPlanName,
container_image_values: { ...values }
},
containerImagesPrepareFinished,
60 * 1000
)
).catch(error => {
dispatch(
stopSubmit('containerImagesPrepareForm', {
_error: {
title: formatMessage(messages.configureImagesFailed),
message: error.message
}
})
);
dispatch(
handleErrors(error, formatMessage(messages.configureImagesFailed), false)
);
});
};
export const containerImagesPrepareFinished = execution => (
dispatch,
getState,
{ getIntl }
) => {
const { formatMessage } = getIntl(getState());
const { output: { message, status, params } } = execution;
if (status === 'SUCCESS') {
dispatch(containerImagesPrepareSuccess(params.ContainerImagePrepare));
dispatch(reset('containerImagesPrepareForm'));
dispatch(stopSubmit('containerImagesPrepareForm'));
} else {
dispatch(
stopSubmit('containerImagesPrepareForm', {
_error: {
title: formatMessage(messages.configureImagesFailed),
message: sanitizeMessage(message)
}
})
);
}
};
export const containerImagesPrepareSuccess = value => ({
type: CONTAINER_IMAGES_PREPARE_SUCCESS,
payload: value
});

View File

@ -55,6 +55,7 @@ import {
getDeploymentFailuresFinished
} from './DeploymentActions';
import { fetchNetworksFinished } from './NetworksActions';
import { containerImagesPrepareFinished } from './ContainerImagesActions';
export const handleAuthenticationSuccess = (message, dispatch) => {
message = get(message, ['body', 'message']);
@ -244,6 +245,16 @@ export const messageReceived = message => (dispatch, getState) => {
break;
}
case MistralConstants.CONTAINER_IMAGES_PREPARE_DEFAULT: {
dispatch(
handleWorkflowMessage(
payload.execution.id,
containerImagesPrepareFinished
)
);
break;
}
default:
break;
}

View File

@ -0,0 +1,313 @@
/**
* Copyright 2018 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 React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { Form, FieldLevelHelp, Wizard } from 'patternfly-react';
import { reduxForm, Field } from 'redux-form';
import { format } from 'redux-form-validators';
import { OverlayLoader } from '../ui/Loader';
import FormErrorList from '../ui/forms/FormErrorList';
import HorizontalInput from '../ui/reduxForm/HorizontalInput';
import { startContainerImagesPrepare } from '../../actions/ContainerImagesActions';
import PushDestinationInput from './PushDestinationInput';
import {
IPV4_WITH_PORT_REGEX,
DOCKER_TAG_REGEX,
DOCKER_REGISTRY_NAMESPACE_REGEX
} from '../../utils/regex';
const messages = defineMessages({
generatingConfiguration: {
id: 'ContainerImagesPrepareForm.generatingConfiguration',
defaultMessage: 'Generating container images configuration...'
},
namespaceLabel: {
id: 'ContainerImagesPrepareForm.namespaceLabel',
defaultMessage: 'Registry Namespace'
},
namespaceInvalid: {
id: 'ContainerImagesPrepareForm.namespaceInvalid',
defaultMessage: 'Provide a valid registry address and images namespace'
},
namePrefixLabel: {
id: 'ContainerImagesPrepareForm.namePrefixLabel',
defaultMessage: 'Name Prefix'
},
nameSuffixLabel: {
id: 'ContainerImagesPrepareForm.nameSuffixLabel',
defaultMessage: 'Name Suffix'
},
tagLabel: {
id: 'ContainerImagesPrepareForm.tagLabel',
defaultMessage: 'Tag'
},
tagFromLabelLabel: {
id: 'ContainerImagesPrepareForm.tagFromLabelLabel',
defaultMessage: 'Tag from Label'
},
pushDestinationLabel: {
id: 'ContainerImagesPrepareForm.pushDestinationLabel',
defaultMessage: 'Push Destination'
},
pushDestinationUndercloud: {
id: 'ContainerImagesPrepareForm.pushDestinationUndercloud',
defaultMessage: 'Undercloud image registry'
},
namespaceDescription: {
id: 'ContainerImagesPrepareForm.namespaceDescription',
defaultMessage:
'Namespace of the remote registry from which the container images will be pulled during deployment.'
},
namePrefixDescription: {
id: 'ContainerImagesPrepareForm.namePrefixDescription',
defaultMessage: 'Container image name prefix'
},
nameSuffixDescription: {
id: 'ContainerImagesPrepareForm.nameSuffixDescription',
defaultMessage: 'Container image name suffix'
},
tagDescription: {
id: 'ContainerImagesPrepareForm.tagDescription',
defaultMessage: 'Tag representing the latest image version.'
},
pushDestinationDescription: {
id: 'ContainerImagesPrepareForm.pushDestinationDescription',
defaultMessage:
'By specifying a Push Destination, the required images will be copied \
from provided namespace to this registry. As part of the undercloud \
install, an image registry is configured on port 8787. This can be used \
to increase reliability of image pulls, and minimise overall network \
transfers. Alternatively it is possible to explicitly specify the \
registry to push the images to.'
},
pushDestinationValidationMessage: {
id: 'ContainerImagesPrepareForm.pushDestinationValidationMessage',
defaultMessage: 'Please enter a valid IPv4 address and port'
},
tagFromLabelDescription: {
id: 'ContainerImagesPrepareForm.tagFromLabelDescription',
defaultMessage:
'Provide a label to discover the versioned tag for images. Some build \
pipelines have a versioned tag which can only be discovered via a \
combination of labels. For this case, a template format can be specified \
instead, e.g. {labelExample}. If you want these parameters to have \
the actual tag instead of the discovered tag, this entry can be omitted.'
}
});
class ContainerImagesPrepareForm extends Component {
componentDidMount() {
const { initialValues: { namespace }, resetToDefaults } = this.props;
if (!namespace) {
resetToDefaults();
}
}
validatePushDestination(value) {
if (typeof value === 'boolean') {
return undefined;
} else if (new RegExp(IPV4_WITH_PORT_REGEX).test(value)) {
return undefined;
} else {
return (
<FormattedMessage {...messages.pushDestinationValidationMessage} />
);
}
}
render() {
const {
error,
intl: { formatMessage },
submitting,
handleSubmit
} = this.props;
return (
<Form onSubmit={handleSubmit} horizontal>
<OverlayLoader
loaded={!submitting}
content={formatMessage(messages.generatingConfiguration)}
>
<Wizard.Row>
<Wizard.Main>
<Wizard.Contents stepIndex={0} activeStepIndex={0}>
<FormErrorList errors={error ? [error] : []} />
<fieldset>
<Field
name="namespace"
component={HorizontalInput}
id="namespace"
label={
<Fragment>
<FormattedMessage {...messages.namespaceLabel} />
<FieldLevelHelp
placement="right"
style={{ maxWidth: 400 }}
content={formatMessage(messages.namespaceDescription)}
/>
</Fragment>
}
labelColumns={4}
validate={format({
with: DOCKER_REGISTRY_NAMESPACE_REGEX,
message: formatMessage(messages.namespaceInvalid),
allowBlank: true
})}
/>
<Field
name="name_prefix"
component={HorizontalInput}
id="name_prefix"
label={
<Fragment>
<FormattedMessage {...messages.namePrefixLabel} />
<FieldLevelHelp
placement="right"
style={{ maxWidth: 400 }}
content={formatMessage(
messages.namePrefixDescription
)}
/>
</Fragment>
}
labelColumns={4}
inputColumns={4}
/>
<Field
name="name_suffix"
component={HorizontalInput}
id="name_suffix"
label={
<Fragment>
<FormattedMessage {...messages.nameSuffixLabel} />
<FieldLevelHelp
placement="right"
style={{ maxWidth: 400 }}
content={formatMessage(
messages.nameSuffixDescription
)}
/>
</Fragment>
}
labelColumns={4}
inputColumns={4}
/>
<Field
name="tag"
component={HorizontalInput}
id="tag"
label={
<Fragment>
<FormattedMessage {...messages.tagLabel} />
<FieldLevelHelp
placement="right"
style={{ maxWidth: 400 }}
content={formatMessage(messages.tagDescription)}
/>
</Fragment>
}
validate={format({
with: DOCKER_TAG_REGEX,
allowBlank: true
})}
labelColumns={4}
inputColumns={4}
/>
</fieldset>
<fieldset>
<Field
name="push_destination"
component={PushDestinationInput}
id="push_destination"
label={
<Fragment>
<FormattedMessage {...messages.pushDestinationLabel} />
<FieldLevelHelp
placement="right"
style={{ maxWidth: 400 }}
content={formatMessage(
messages.pushDestinationDescription
)}
/>
</Fragment>
}
labelColumns={4}
validate={this.validatePushDestination}
/>
<Field
name="tag_from_label"
component={HorizontalInput}
id="tag_from_label"
label={
<Fragment>
<FormattedMessage {...messages.tagFromLabelLabel} />
<FieldLevelHelp
placement="right"
style={{ maxWidth: 400 }}
content={formatMessage(
messages.tagFromLabelDescription,
{
labelExample: '{version}-{release}'
}
)}
/>
</Fragment>
}
labelColumns={4}
inputColumns={4}
/>
</fieldset>
</Wizard.Contents>
</Wizard.Main>
</Wizard.Row>
</OverlayLoader>
</Form>
);
}
}
ContainerImagesPrepareForm.propTypes = {
error: PropTypes.object,
handleSubmit: PropTypes.func.isRequired,
initialValues: PropTypes.object.isRequired,
intl: PropTypes.object.isRequired,
resetToDefaults: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired
};
const normalizeValues = values => {
const generateIfEmpty = ['namespace', 'tag', 'push_destination'];
Object.keys(values).map(key => {
if (generateIfEmpty.includes(key)) {
!values[key] && delete values[key];
}
});
return values;
};
const form = reduxForm({
form: 'containerImagesPrepareForm',
onSubmit: (values, dispatch, props) => {
normalizeValues(values);
dispatch(startContainerImagesPrepare(values));
},
touchOnChange: true,
enableReinitialize: true
});
export default injectIntl(form(ContainerImagesPrepareForm));

View File

@ -0,0 +1,84 @@
/**
* Copyright 2018 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 React, { Fragment } from 'react';
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { Icon, Button } from 'patternfly-react';
import { submit, isSubmitting, isPristine, isInvalid } from 'redux-form';
import { startContainerImagesPrepare } from '../../actions/ContainerImagesActions';
const messages = defineMessages({
next: {
id: 'ContainerImagesWizard.next',
defaultMessage: 'Next'
},
reset: {
id: 'ContainerImagesPrepareFormActions.reset',
defaultMessage: 'Reset to Defaults'
}
});
const ContainerImagesPrepareFormActions = ({
isSubmitting,
isInvalid,
isPristine,
resetToDefaults,
submitImagesPrepareForm
}) => (
<Fragment>
<Button onClick={resetToDefaults} disabled={isSubmitting}>
<FormattedMessage {...messages.reset} />
</Button>
<Button
bsStyle="primary"
onClick={submitImagesPrepareForm}
disabled={isSubmitting || isPristine || isInvalid}
>
<FormattedMessage {...messages.next} />
<Icon type="fa" name="angle-right" />
</Button>
</Fragment>
);
ContainerImagesPrepareFormActions.propTypes = {
intl: PropTypes.object.isRequired,
isFetchingParameters: PropTypes.bool.isRequired,
isInvalid: PropTypes.bool.isRequired,
isPristine: PropTypes.bool.isRequired,
isSubmitting: PropTypes.bool.isRequired,
resetToDefaults: PropTypes.func.isRequired,
submitImagesPrepareForm: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
isFetchingParameters: state.parameters.isFetching,
isSubmitting: isSubmitting('containerImagesPrepareForm')(state),
isPristine: isPristine('containerImagesPrepareForm')(state),
isInvalid: isInvalid('containerImagesPrepareForm')(state)
});
const mapDispatchToProps = dispatch => ({
resetToDefaults: () => dispatch(startContainerImagesPrepare({})),
submitImagesPrepareForm: () => dispatch(submit('containerImagesPrepareForm'))
});
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(
ContainerImagesPrepareFormActions
)
);

View File

@ -0,0 +1,213 @@
/**
* Copyright 2018 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 React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { Wizard, Modal, Icon, Button } from 'patternfly-react';
import { checkRunningDeployment } from '../utils/checkRunningDeploymentHOC';
import { getCurrentPlanName } from '../../selectors/plans';
import { Loader } from '../ui/Loader';
import { fetchParameters } from '../../actions/ParametersActions';
import { startContainerImagesPrepare } from '../../actions/ContainerImagesActions';
import {
RoutedWizard,
CloseModalXButton,
CloseModalButton
} from '../ui/Modals';
import ContainerImagesPrepareForm from './ContainerImagesPrepareForm';
import { getContainerImagePrepareParameterSeed } from '../../selectors/parameters';
import ContainerImagesPrepareFormActions from './ContainerImagesPrepareFormActions';
const messages = defineMessages({
close: {
id: 'ContainerImagesWizard.close',
defaultMessage: 'Close'
},
cancel: {
id: 'ContainerImagesWizard.cancel',
defaultMessage: 'Cancel'
},
back: {
id: 'ContainerImagesWizard.back',
defaultMessage: 'Back'
},
save: {
id: 'ContainerImagesWizard.save',
defaultMessage: 'Save Changes'
},
title: {
id: 'ContainerImagesWizard.title',
defaultMessage: 'Prepare Container Images'
},
loadingData: {
id: 'ContainerImagesWizard.loadingData',
defaultMessage: 'Loading configuration...'
},
configureImages: {
id: 'ContainerImagesWizard.configureImages',
defaultMessage: 'Configure Images'
},
review: {
id: 'ContainerImagesWizard.review',
defaultMessage: 'Review Configuration'
}
});
class ContainerImagesWizard extends Component {
state = {
activeStepIndex: 0
};
componentDidMount() {
const {
currentPlanName,
fetchParameters,
isFetchingParameters
} = this.props;
!isFetchingParameters && fetchParameters(currentPlanName);
}
setActiveStepIndex(index) {
this.setState({ activeStepIndex: index });
}
render() {
const {
currentPlanName,
intl: { formatMessage },
isFetchingParameters,
containerImagePrepareParameterSeed,
resetToDefaults
} = this.props;
const { activeStepIndex } = this.state;
const steps = [
{
step: 1,
label: '1',
title: formatMessage(messages.configureImages)
},
{
step: 2,
label: '2',
title: formatMessage(messages.review)
}
];
return (
<RoutedWizard
id="ContainerImagesWizard__Wizard"
redirectPath={`/plans/${currentPlanName}`}
>
<Modal.Header>
<CloseModalXButton />
<Modal.Title>
<FormattedMessage {...messages.title} />
</Modal.Title>
</Modal.Header>
<Wizard.Body>
<Wizard.Steps
steps={steps.map(({ step, label, title }, index) => (
<Wizard.Step
key={step}
stepIndex={index}
step={step}
label={label}
title={title}
activeStep={steps[activeStepIndex].step}
onClick={() => this.setActiveStepIndex(index)}
/>
))}
/>
<Loader
height={60}
loaded={!isFetchingParameters}
content={formatMessage(messages.loadingData)}
>
{activeStepIndex === 0 ? (
<ContainerImagesPrepareForm
onSubmit={this.handleContainerImagesPrepareFormSubmit}
initialValues={{
...containerImagePrepareParameterSeed,
push_destination:
containerImagePrepareParameterSeed.push_destination || false
}}
resetToDefaults={resetToDefaults}
/>
) : (
<p>parameter form here</p>
)}
</Loader>
</Wizard.Body>
<Wizard.Footer>
<CloseModalButton>
<FormattedMessage {...messages.cancel} />
</CloseModalButton>
{activeStepIndex === 0 && <ContainerImagesPrepareFormActions />}
{activeStepIndex === 1 && (
<Fragment>
<Button
bsStyle="default"
onClick={() => this.setActiveStepIndex(0)}
>
<Icon type="fa" name="angle-left" />
<FormattedMessage {...messages.back} />
</Button>
<Button bsStyle="primary" onClick={this.onNextButtonClick}>
<FormattedMessage {...messages.save} />
</Button>
<CloseModalButton>
<FormattedMessage {...messages.close} />
</CloseModalButton>
</Fragment>
)}
</Wizard.Footer>
</RoutedWizard>
);
}
}
ContainerImagesWizard.propTypes = {
containerImagePrepareParameterSeed: PropTypes.object.isRequired,
currentPlanName: PropTypes.string.isRequired,
fetchParameters: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
isFetchingParameters: PropTypes.bool.isRequired,
resetToDefaults: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
containerImagePrepareParameterSeed: getContainerImagePrepareParameterSeed(
state
),
currentPlanName: getCurrentPlanName(state),
isFetchingParameters: state.parameters.isFetching
});
const mapDispatchToProps = dispatch => ({
fetchParameters: currentPlanName =>
dispatch(fetchParameters(currentPlanName)),
resetToDefaults: () => dispatch(startContainerImagesPrepare({}))
});
export default checkRunningDeployment(
injectIntl(
connect(mapStateToProps, mapDispatchToProps)(ContainerImagesWizard)
)
);

View File

@ -0,0 +1,129 @@
/**
* 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 React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage } from 'react-intl';
import {
FormControl,
Radio,
FormGroup,
ControlLabel,
Col
} from 'patternfly-react';
import cx from 'classnames';
import {
getValidationState,
InputDescription,
InputMessage
} from '../ui/reduxForm/utils';
const messages = defineMessages({
pushDestinationOptionFalse: {
id: 'PushDestinationInput.pushDestinationOptionFalse',
defaultMessage: `Don't push images`
},
pushDestinationOptionTrue: {
id: 'PushDestinationInput.pushDestinationOptionTrue',
defaultMessage: 'Push to Undercloud registry'
},
pushDestinationOptionCustom: {
id: 'PushDestinationInput.pushDestinationOptionCustom',
defaultMessage: 'Specify custom registry'
}
});
export default class PushDestinationInput extends React.Component {
customRegistryInputRef = createRef();
render() {
const {
id,
inputColumns,
labelColumns,
meta,
label,
input: { value, onChange, onBlur, ...inputRest },
description,
required
} = this.props;
return (
<FormGroup controlId={id} validationState={getValidationState(meta)}>
<Col
componentClass={ControlLabel}
sm={labelColumns}
className={cx({ 'required-pf': required })}
>
{label}
</Col>
<Col sm={inputColumns}>
<Radio
{...inputRest}
checked={value === false}
onChange={e => onChange(false)}
onBlur={e => onBlur(false)}
>
<FormattedMessage {...messages.pushDestinationOptionFalse} />
</Radio>
<Radio
{...inputRest}
checked={value === true}
onChange={e => onChange(true)}
onBlur={e => onBlur(true)}
>
<FormattedMessage {...messages.pushDestinationOptionTrue} />
</Radio>
<Radio
{...inputRest}
checked={value === this.customRegistryInputRef.value}
onChange={e => onChange(this.customRegistryInputRef.value)}
onBlur={e => onBlur(this.customRegistryInputRef.value)}
>
<FormattedMessage {...messages.pushDestinationOptionCustom} />
</Radio>
<FormControl
{...inputRest}
defaultValue="192.168.24.1:8787"
inputRef={ref => (this.customRegistryInputRef = ref)}
style={{ marginTop: 4 }}
type="text"
onChange={e => onChange(e.target.value)}
onBlur={e => onBlur(e.target.value)}
disabled={value !== this.customRegistryInputRef.value}
/>
<InputMessage {...meta} />
<InputDescription description={description} />
</Col>
</FormGroup>
);
}
}
PushDestinationInput.propTypes = {
description: PropTypes.node,
id: PropTypes.string.isRequired,
input: PropTypes.object.isRequired,
inputColumns: PropTypes.number.isRequired,
label: PropTypes.node,
labelColumns: PropTypes.number.isRequired,
meta: PropTypes.object.isRequired,
required: PropTypes.bool.isRequired
};
PushDestinationInput.defaultProps = {
labelColumns: 4,
inputColumns: 7,
required: false
};

View File

@ -43,6 +43,7 @@ import { fetchEnvironmentConfiguration } from '../../actions/EnvironmentConfigur
import HardwareStep from './HardwareStep';
import { Loader } from '../ui/Loader';
import NetworkConfiguration from '../networkConfiguration/NetworkConfiguration';
import ContainerImagesWizard from '../containerImages/ContainerImagesWizard';
import { fetchParameters } from '../../actions/ParametersActions';
import RoleDetail from '../roles/RoleDetail';
import RolesStep from './RolesStep';
@ -245,6 +246,10 @@ class CurrentPlan extends React.Component {
path="/plans/:planName/network-configuration"
component={NetworkConfiguration}
/>
<Route
path="/plans/:planName/container-images"
component={ContainerImagesWizard}
/>
<Route
path="/plans/:planName/deployment-confirmation"
render={() => (

View File

@ -0,0 +1,25 @@
/**
* Copyright 2018 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 React from 'react';
import RoutedModal from './RoutedModal';
const RoutedWizard = props => (
<RoutedModal {...props} dialogClassName="modal-lg wizard-pf" />
);
export default RoutedWizard;

View File

@ -19,4 +19,5 @@ export ConfirmationModal from './ConfirmationModal';
export Modal from './Modal';
export ModalPanel from './ModalPanel';
export RoutedModal from './RoutedModal';
export RoutedWizard from './RoutedWizard';
export RoutedModalPanel from './RoutedModalPanel';

View File

@ -0,0 +1,18 @@
/**
* Copyright 2018 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 const CONTAINER_IMAGES_PREPARE_SUCCESS =
'CONTAINER_IMAGES_PREPARE_SUCCESS';

View File

@ -27,6 +27,8 @@ export default {
CAPABILITIES_UPDATE: 'tripleo.heat_capabilities.update',
CREATE_CONTAINER: 'tripleo.plan.create_container',
CONFIG_DOWNLOAD_DEPLOY: 'tripleo.deployment.v1.config_download_deploy',
CONTAINER_IMAGES_PREPARE_DEFAULT:
'tripleo.container_images.v1.container_image_prepare_default',
DEPLOYMENT_DEPLOY_PLAN: 'tripleo.deployment.v1.deploy_plan',
UNDEPLOY_PLAN: 'tripleo.deployment.v1.undeploy_plan',
RECOVER_DEPLOYMENT_STATUS: 'tripleo.deployment.v1.recover_deployment_status',

View File

@ -24,6 +24,7 @@ import {
Resource,
Parameter
} from '../immutableRecords/parameters';
import { CONTAINER_IMAGES_PREPARE_SUCCESS } from '../constants/ContainerImagesConstants';
const initialState = new ParametersDefaultState();
@ -55,6 +56,12 @@ export default function parametersReducer(state = initialState, action) {
case EnvironmentConfigurationConstants.UPDATE_ENVIRONMENT_CONFIGURATION_SUCCESS:
return state.set('loaded', false);
case CONTAINER_IMAGES_PREPARE_SUCCESS:
return state.updateIn(
['parameters', 'ContainerImagePrepare'],
p => p && p.set('default', action.payload)
);
case PlansConstants.PLAN_CHOSEN:
return initialState;

View File

@ -33,3 +33,15 @@ export const FQDN_REGEX = new RegExp(
export const PORT_REGEX = new RegExp(
/^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/
);
export const DOCKER_TAG_REGEX = new RegExp(/^[\w][\w.-]{0,127}$/);
// FQDN or IPV4:PORT / DOCKER_TAG (Docker tag format is the same as image name format)
export const DOCKER_REGISTRY_NAMESPACE_REGEX = new RegExp(
/^(((?=[a-z0-9-]{1,63}\.)[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))\/[\w][\w.-]{0,127}$/i
);
// IPV4:PORT
export const IPV4_WITH_PORT_REGEX = new RegExp(
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/
);