fuel-web/nailgun/static/views/custom_controls.js

364 lines
12 KiB
JavaScript

/*
* Copyright 2015 Mirantis, 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 _ from 'underscore';
import i18n from 'i18n';
import React from 'react';
import ReactDOM from 'react-dom';
import utils from 'utils';
import {Input, Tooltip} from 'views/controls';
var customControls = {};
customControls.custom_repo_configuration = React.createClass({
statics: {
// validate method represented as static method to support cluster settings validation
validate(setting, models) {
var ns = 'cluster_page.settings_tab.custom_repo_configuration.errors.';
var nameRegexp = /^[\w-.]+$/;
var os = models.release.get('operating_system');
var errors = setting.value.map((repo) => {
var error = {};
var value = this.repoToString(repo, os);
if (!repo.name) {
error.name = i18n(ns + 'empty_name');
} else if (!repo.name.match(nameRegexp)) {
error.name = i18n(ns + 'invalid_name');
}
if (!value || !value.match(this.defaultProps.repoRegexes[os])) {
error.uri = i18n(ns + 'invalid_repo');
}
var priority = repo.priority;
if (
_.isNaN(priority) ||
!_.isNull(priority) && (
!(priority === _.parseInt(priority, 10)) ||
os === 'CentOS' && (priority < 1 || priority > 99)
)
) {
error.priority = i18n(ns + 'invalid_priority');
}
return _.isEmpty(error) ? null : error;
});
return _.compact(errors).length ? errors : null;
},
repoToString(repo, os) {
var repoData = _.compact(this.defaultProps.repoAttributes[os].map(
(attribute) => repo[attribute]
));
if (!repoData.length) return ''; // in case of new repo
return repoData.join(' ');
}
},
getInitialState() {
return {};
},
getDefaultProps() {
return {
/* eslint-disable max-len */
repoRegexes: {
Ubuntu: /^(deb|deb-src)\s+(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s+([\w\-.\/]+)(?:\s+([\w\-.\/\s]+))?$/i,
CentOS: /^(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s*$/i
},
/* eslint-enable max-len */
repoAttributes: {
Ubuntu: ['type', 'uri', 'suite', 'section'],
CentOS: ['uri']
}
};
},
changeRepos(method, index, value) {
value = _.trim(value).replace(/\s+/g, ' ');
var repos = _.cloneDeep(this.props.value);
var os = this.props.cluster.get('release').get('operating_system');
switch (method) {
case 'add':
var data = {
name: '',
type: '',
uri: '',
priority: this.props.extra_priority
};
if (os === 'Ubuntu') {
data.suite = '';
data.section = '';
} else {
data.type = 'rpm';
}
repos.push(data);
break;
case 'delete':
repos.splice(index, 1);
this.setState({key: _.now()});
break;
case 'change_name':
repos[index].name = value;
break;
case 'change_priority':
repos[index].priority = value === '' ? null : Number(value);
break;
default:
var repo = repos[index];
var match = value.match(this.props.repoRegexes[os]);
if (match) {
_.each(this.props.repoAttributes[os], (attribute, index) => {
repo[attribute] = match[index + 1] || '';
});
} else {
repo.uri = value;
}
}
var path = this.props.settings.makePath(this.props.path, 'value');
this.props.settings.set(path, repos);
this.props.settings.isValid({models: this.props.configModels});
},
renderDeleteButton(index) {
return (
<button
className='btn btn-link'
onClick={this.changeRepos.bind(this, 'delete', index)}
disabled={this.props.disabled}
>
<i className='glyphicon glyphicon-minus-sign' />
</button>
);
},
render() {
var ns = 'cluster_page.settings_tab.custom_repo_configuration.';
var os = this.props.cluster.get('release').get('operating_system');
return (
<div className='repos' key={this.state.key}>
{this.props.description &&
<span
className='help-block'
dangerouslySetInnerHTML={{
__html: utils.urlify(utils.linebreaks(_.escape(this.props.description)))
}}
/>
}
{this.props.value.map((repo, index) => {
var error = (this.props.error || {})[index];
var props = {
name: index,
type: 'text',
disabled: this.props.disabled
};
return (
<div className='form-inline' key={'repo-' + index}>
<Input
{...props}
defaultValue={repo.name}
error={error && error.name}
wrapperClassName='repo-name'
onChange={this.changeRepos.bind(this, 'change_name')}
label={index === 0 && i18n(ns + 'labels.name')}
debounce
/>
<Input
{...props}
defaultValue={this.constructor.repoToString(repo, os)}
error={error && (error.uri ? error.name ? '' : error.uri : null)}
onChange={this.changeRepos.bind(this, null)}
label={index === 0 && i18n(ns + 'labels.uri')}
wrapperClassName='repo-uri'
debounce
/>
<Input
{...props}
defaultValue={repo.priority}
error={
error && (error.priority ? (error.name || error.uri) ? '' : error.priority : null)
}
wrapperClassName='repo-priority'
onChange={this.changeRepos.bind(this, 'change_priority')}
extraContent={index > 0 && this.renderDeleteButton(index)}
label={index === 0 && i18n(ns + 'labels.priority')}
placeholder={i18n(ns + 'placeholders.priority')}
debounce
/>
</div>
);
})}
<div className='buttons'>
<button
key='addExtraRepo'
className='btn btn-default btn-add-repo'
onClick={this.changeRepos.bind(this, 'add')}
disabled={this.props.disabled}
>
{i18n(ns + 'add_repo_button')}
</button>
</div>
</div>
);
}
});
customControls.text_list = customControls.textarea_list = React.createClass({
statics: {
validate(setting) {
if (!(setting.regex || {}).source) return null;
var regex = new RegExp(setting.regex.source);
var errors = _.map(setting.value,
(value) => value.match(regex) ? null : setting.regex.error
);
return _.compact(errors).length ? errors : null;
}
},
propTypes: {
value: React.PropTypes.arrayOf(React.PropTypes.node).isRequired,
type: React.PropTypes.oneOf(['text_list', 'textarea_list']).isRequired,
name: React.PropTypes.node,
label: React.PropTypes.node,
description: React.PropTypes.node,
error: React.PropTypes.arrayOf(React.PropTypes.node),
disabled: React.PropTypes.bool,
wrapperClassName: React.PropTypes.node,
onChange: React.PropTypes.func,
min: React.PropTypes.number,
max: React.PropTypes.number,
tooltipPlacement: React.PropTypes.oneOf(['left', 'right', 'top', 'bottom']),
tooltipIcon: React.PropTypes.node,
tooltipText: React.PropTypes.node
},
getInitialState() {
return {};
},
getDefaultProps() {
return {
min: 1,
max: null,
tooltipIcon: 'glyphicon-warning-sign',
tooltipPlacement: 'right'
};
},
changeField(index, method = 'change') {
var value = _.clone(this.props.value);
switch (method) {
case 'add':
value.splice(index + 1, 0, '');
this.setState({key: _.now()});
break;
case 'remove':
value.splice(index, 1);
this.setState({key: _.now()});
break;
case 'change':
var input = ReactDOM.findDOMNode(this.refs['input' + index]);
value[index] = input.value;
break;
}
if (this.props.onChange) return this.props.onChange(this.props.name, value);
},
debouncedFieldChange: _.debounce(function(index) {
return this.changeField(index);
}, 200, {leading: true}),
renderMultipleInputControls(index) {
return (
<div className='field-controls'>
{(!this.props.max || this.props.value.length < this.props.max) &&
<button
ref={'add' + index}
className='btn btn-link btn-add-field'
disabled={this.props.disabled}
onClick={() => this.changeField(index, 'add')}
>
<i className='glyphicon glyphicon-plus-sign' />
</button>
}
{this.props.value.length > this.props.min &&
<button
ref={'remove' + index}
className='btn btn-link btn-remove-field'
disabled={this.props.disabled}
onClick={() => this.changeField(index, 'remove')}
>
<i className='glyphicon glyphicon-minus-sign' />
</button>
}
</div>
);
},
renderInput(value, index) {
var error = (this.props.error || [])[index] || null;
var Tag = this.props.type === 'textarea_list' ? 'textarea' : 'input';
return (
<div
key={'input' + index}
className={utils.classNames({'has-error': !_.isNull(error)})}
>
<Tag
{... _.pick(this.props, 'name', 'disabled')}
ref={'input' + index}
type='text'
className='form-control'
onChange={() => this.debouncedFieldChange(index)}
defaultValue={value}
/>
{this.renderMultipleInputControls(index)}
{error &&
<div className='help-block field-error'>{error}</div>
}
</div>
);
},
renderLabel() {
if (!this.props.label) return null;
return (
<label key='label'>
{this.props.label}
{this.props.tooltipText &&
<Tooltip text={this.props.tooltipText} placement={this.props.tooltipPlacement}>
<i className={utils.classNames('glyphicon tooltip-icon', this.props.tooltipIcon)} />
</Tooltip>
}
</label>
);
},
renderDescription() {
if (this.props.error) return null;
return (
<span key='description' className='help-block field-description'>
{this.props.description}
</span>
);
},
renderWrapper(children) {
return (
<div
key={this.state.key}
className={utils.classNames({
'form-group': true,
disabled: this.props.disabled,
[this.props.wrapperClassName]: this.props.wrapperClassName
})}
>
{children}
</div>
);
},
render() {
return this.renderWrapper([
this.renderLabel(),
<div key='field-list' className='field-list'>
{_.map(this.props.value, this.renderInput)}
</div>,
this.renderDescription()
]);
}
});
export default customControls;