From d1f3c524112afb9b3d86f202cc6c1708c72aeccd Mon Sep 17 00:00:00 2001 From: Julia Aranovich Date: Wed, 22 Oct 2014 15:05:52 +0400 Subject: [PATCH] Fuel Welcome page Closes-Bug: #1377834 Related-Bug: #1377846 Change-Id: I9cbd77166ea49a0e553d89dade887236e6753898 --- .../fixtures/master_node_settings.yaml | 69 ++-- .../nailgun/objects/master_node_settings.py | 16 +- .../test_master_node_settings_handler.py | 303 +++++++++++++++--- nailgun/static/css/styles.less | 98 ++++++ nailgun/static/i18n/translation.json | 68 +++- nailgun/static/js/app.js | 15 +- nailgun/static/js/models.js | 64 ++++ nailgun/static/js/views/controls.jsx | 33 +- nailgun/static/js/views/login_page.jsx | 15 +- nailgun/static/js/views/statistics_mixin.jsx | 181 +++++++++++ nailgun/static/js/views/support_page.jsx | 36 ++- nailgun/static/js/views/welcome_page.jsx | 90 ++++++ 12 files changed, 883 insertions(+), 105 deletions(-) create mode 100644 nailgun/static/js/views/statistics_mixin.jsx create mode 100644 nailgun/static/js/views/welcome_page.jsx diff --git a/nailgun/nailgun/fixtures/master_node_settings.yaml b/nailgun/nailgun/fixtures/master_node_settings.yaml index 21ca96e8b4..42e073060c 100644 --- a/nailgun/nailgun/fixtures/master_node_settings.yaml +++ b/nailgun/nailgun/fixtures/master_node_settings.yaml @@ -2,21 +2,54 @@ - pk: 1 model: "nailgun.master_node_settings" fields: - master_node_uid: "62410d05-ecbd-4912-83fe-5db51ebb273e" - settings: - send_anonymous_statistic: - type: "checkbox" - value: true - send_user_info: - type: "checkbox" - value: true - user_info: - name: - type: "text" - value: "Test User" - email: - type: "text" - value: "test@email.com" - company: - type: "text" - value: "Test Company" + master_node_uid: "62410d05-ecbd-4912-83fe-5db51ebb273e" + settings: + statistics: + send_anonymous_statistic: + type: "checkbox" + value: true + label: "statistics.setting_labels.send_anonymous_statistic" + weight: 10 + send_user_info: + type: "checkbox" + value: true + label: "statistics.setting_labels.send_user_info" + weight: 20 + restrictions: + - "fuel_settings:statistics.send_anonymous_statistic.value == false" + - condition: &commumity_iso "not ('mirantis' in version:feature_groups)" + action: "hide" + name: + type: "text" + value: "" + label: "statistics.setting_labels.name" + weight: 30 + regex: + source: &non_empty_string '\S' + error: "statistics.errors.name" + restrictions: &user_info_restrictions + - "fuel_settings:statistics.send_anonymous_statistic.value == false" + - "fuel_settings:statistics.send_user_info.value == false" + - condition: *commumity_iso + action: "hide" + email: + type: "text" + value: "" + label: "statistics.setting_labels.email" + weight: 40 + regex: + source: *non_empty_string + error: "statistics.errors.email" + restrictions: *user_info_restrictions + company: + type: "text" + value: "" + label: "statistics.setting_labels.company" + weight: 50 + regex: + source: *non_empty_string + error: "statistics.errors.company" + restrictions: *user_info_restrictions + user_choice_saved: + type: "hidden" + value: false diff --git a/nailgun/nailgun/objects/master_node_settings.py b/nailgun/nailgun/objects/master_node_settings.py index 704c369290..e881ad78f7 100644 --- a/nailgun/nailgun/objects/master_node_settings.py +++ b/nailgun/nailgun/objects/master_node_settings.py @@ -35,21 +35,7 @@ class MasterNodeSettings(NailgunObject): "description": "Serialized ActionLog object", "type": "object", "properties": { - "settings": { - "type": "object", - "properties": { - "send_anonymous_statistic": {"type": "object"}, - "send_user_info": {"type": "object"}, - "user_info": { - "type": "object", - "properties": { - "name": {"type": "object"}, - "company": {"type": "object"}, - "email": {"type": "object"} - } - } - } - } + "settings": {"type": "object"} } } diff --git a/nailgun/nailgun/test/integration/test_master_node_settings_handler.py b/nailgun/nailgun/test/integration/test_master_node_settings_handler.py index 6f1bc0aa68..d537856bd9 100644 --- a/nailgun/nailgun/test/integration/test_master_node_settings_handler.py +++ b/nailgun/nailgun/test/integration/test_master_node_settings_handler.py @@ -27,26 +27,95 @@ class TestMasterNodeSettingsHandler(BaseIntegrationTest): # fixture file which is located in fixtures directory for nailgun expected = { "settings": { - "send_anonymous_statistic": { - "type": "checkbox", - "value": True - }, - "send_user_info": { - "type": "checkbox", - "value": True - }, - "user_info": { + "statistics": { + "send_anonymous_statistic": { + "type": "checkbox", + "value": True, + "label": "statistics.setting_labels." + "send_anonymous_statistic", + "weight": 10 + }, + "send_user_info": { + "type": "checkbox", + "value": True, + "label": "statistics.setting_labels.send_user_info", + "weight": 20, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] + }, "name": { "type": "text", - "value": "Test User" - }, - "company": { - "type": "text", - "value": "Test Company" + "value": "", + "label": "statistics.setting_labels.name", + "weight": 30, + "regex": { + "source": "\S", + "error": "statistics.errors.name" + }, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + "fuel_settings:statistics." + "send_user_info.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] }, "email": { "type": "text", - "value": "test@email.com" + "value": "", + "label": "statistics.setting_labels.email", + "weight": 40, + "regex": { + "source": "\S", + "error": "statistics.errors.email" + }, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + "fuel_settings:statistics." + "send_user_info.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] + }, + "company": { + "type": "text", + "value": "", + "label": "statistics.setting_labels.company", + "weight": 50, + "regex": { + "source": "\S", + "error": "statistics.errors.company" + }, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + "fuel_settings:statistics." + "send_user_info.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] + }, + "user_choice_saved": { + "type": "hidden", + "value": False } } } @@ -61,26 +130,95 @@ class TestMasterNodeSettingsHandler(BaseIntegrationTest): def test_put_controller(self): data = { "settings": { - "send_anonymous_statistic": { - "type": "checkbox", - "value": False - }, - "send_user_info": { - "type": "checkbox", - "value": True - }, - "user_info": { + "statistics": { + "send_anonymous_statistic": { + "type": "checkbox", + "value": False, + "label": "statistics.setting_labels." + "send_anonymous_statistic", + "weight": 10 + }, + "send_user_info": { + "type": "checkbox", + "value": True, + "label": "statistics.setting_labels.send_user_info", + "weight": 20, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] + }, "name": { "type": "text", - "value": "Some User" - }, - "company": { - "type": "text", - "value": "Some Company" + "value": "Some User", + "label": "statistics.setting_labels.name", + "weight": 30, + "regex": { + "source": "\S", + "error": "statistics.errors.name" + }, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + "fuel_settings:statistics." + "send_user_info.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] }, "email": { "type": "text", - "value": "user@email.com" + "value": "user@email.com", + "label": "statistics.setting_labels.email", + "weight": 40, + "regex": { + "source": "\S", + "error": "statistics.errors.email" + }, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + "fuel_settings:statistics." + "send_user_info.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] + }, + "company": { + "type": "text", + "value": "Some Company", + "label": "statistics.setting_labels.company", + "weight": 50, + "regex": { + "source": "\S", + "error": "statistics.errors.company" + }, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + "fuel_settings:statistics." + "send_user_info.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] + }, + "user_choice_saved": { + "type": "hidden", + "value": True } } } @@ -102,7 +240,7 @@ class TestMasterNodeSettingsHandler(BaseIntegrationTest): def test_patch_controller(self): data = { "settings": { - "user_info": { + "statistics": { "company": { "value": "Other Company" } @@ -112,26 +250,95 @@ class TestMasterNodeSettingsHandler(BaseIntegrationTest): expected = { "settings": { - "send_anonymous_statistic": { - "type": "checkbox", - "value": True - }, - "send_user_info": { - "type": "checkbox", - "value": True - }, - "user_info": { + "statistics": { + "send_anonymous_statistic": { + "type": "checkbox", + "value": True, + "label": "statistics.setting_labels." + "send_anonymous_statistic", + "weight": 10 + }, + "send_user_info": { + "type": "checkbox", + "value": True, + "label": "statistics.setting_labels.send_user_info", + "weight": 20, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] + }, "name": { "type": "text", - "value": "Test User" - }, - "company": { - "type": "text", - "value": "Other Company" + "value": "", + "label": "statistics.setting_labels.name", + "weight": 30, + "regex": { + "source": "\S", + "error": "statistics.errors.name" + }, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + "fuel_settings:statistics." + "send_user_info.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] }, "email": { "type": "text", - "value": "test@email.com" + "value": "", + "label": "statistics.setting_labels.email", + "weight": 40, + "regex": { + "source": "\S", + "error": "statistics.errors.email" + }, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + "fuel_settings:statistics." + "send_user_info.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] + }, + "company": { + "type": "text", + "value": "Other Company", + "label": "statistics.setting_labels.company", + "weight": 50, + "regex": { + "source": "\S", + "error": "statistics.errors.company" + }, + "restrictions": [ + "fuel_settings:statistics." + "send_anonymous_statistic.value == false", + "fuel_settings:statistics." + "send_user_info.value == false", + { + "condition": + "not ('mirantis' in version:feature_groups)", + "action": "hide" + } + ] + }, + "user_choice_saved": { + "type": "hidden", + "value": False } } } @@ -153,9 +360,7 @@ class TestMasterNodeSettingsHandler(BaseIntegrationTest): def test_validation_error(self): data = { - "settings": { - "send_user_info": "I'm not an object, bro:)" - } + "settings": "I'm not an object, bro:)" } resp = self.app.put( diff --git a/nailgun/static/css/styles.less b/nailgun/static/css/styles.less index b8a3d9b0db..66a65bb824 100644 --- a/nailgun/static/css/styles.less +++ b/nailgun/static/css/styles.less @@ -3593,3 +3593,101 @@ i.btn-cluster-details { } } } + + +/** Fuel Welcome page **/ + +.welcome-page { + .width-height-mixin; + .box-sizing-mixin; + padding: 40px; + color: #4d545b; + > div { + width: 800px; + background-color: @fuel-white-color; + margin: 0 auto; + padding: 30px; + padding-bottom: 10px; + .box-sizing-mixin; + .border-radius-mixin(10px); + .center { text-align: center; } + h2 { + line-height: 40px; + .font-layout-mixin(36px, #4d545b); + .indent-mixin(); + margin-bottom: 20px; + } + .welcome-checkbox-box { + text-align: center; + margin-bottom: 16px; + padding-top: 4px; + div { + display: inline-block; + margin: 0 auto; + } + /* overrides for reusable inputs */ + .parameter-name, .custom-tumbler { + float: none; + font-weight: normal; + } + .label-wrapper { .position(relative, @top: -7px, @left: 3px); } + } + .welcome-form-box { + margin-bottom: 15px; + .welcome-form-item { + display: inline-block; + margin-left: 175px; + font-weight: normal; + .input-label { width: 80px; } + } + } + .welcome-form-error { + text-align: center; + color: @fuel-deep-red; + margin-bottom: 20px; + } + .welcome-button-box { + text-align: center; + button { .width-height-mixin(230px, 44px); } + } + } +} + +.statistics-settings { + margin-bottom: 10px; + /* overrides for reusable inputs */ + .checkbox-or-radio .parameter-name { float: none; } + .parameter-name { + font-weight: normal; + margin: 0; + .label-wrapper { .position(relative, @top: 7px); } + padding: 4px; + } + .parameter-description { margin-top: 10px; } + .input-label { width: 80px; } +} + +.statistics-text-box { + width: 100%; + margin-bottom: 20px; + .font-layout-mixin(14px, #4d545b, lighter); + a { cursor: pointer; } + p { + text-align: justify; + margin: 0px; + margin-bottom: 20px; + } +} +.statistics-disclaimer-box { + background-color: #ecf0f2; + border: 1px solid #c9cdce; + overflow-y: auto; + padding: 20px; + margin-bottom: 16px; + line-height: 16px; + .width-height-mixin(100%, 300px); + .font-layout-mixin(14px, #5b636b, lighter); + .box-sizing-mixin; + p { margin-bottom: 10px; } + ul { margin-bottom: 20px; } +} diff --git a/nailgun/static/i18n/translation.json b/nailgun/static/i18n/translation.json index 7539441e5f..d51fe42199 100644 --- a/nailgun/static/i18n/translation.json +++ b/nailgun/static/i18n/translation.json @@ -79,6 +79,70 @@ "log_in": "Log in", "title": "Log In" }, + "statistics": { + "help_to_improve": "Help us to improve your experience by sending Mirantis information about the settings, features, and deployment actions when you use Mirantis OpenStack.", + "statistics_includes": "Usage statistics include information such as settings, button/menu clicks, hardware configuration, and version information. The usage statistics do not include information such as passwords, ip addresses, or node names. For a complete list of statistics that we gather, ", + "click_here": "click here", + "privacy_policy": "Mirantis’ privacy policy (\"Privacy Policy\") describes our practices regarding the information we collect on the Mirantis web sites and through the use of our products and services, and how it is used and shared with third parties. You can read the policy ", + "privacy_policy_link": "here", + "statistics_includes_full": "Usage statistics include the following. Information such as passwords, ip addresses, and node names are not included.", + "actions_title": "Actions:", + "actions": { + "operation_type": "Operation type (adding cluster, adding node, deployment, removing node, e.t.c.)", + "operation_time": "Operation start and finish time", + "actual_time": "Actual time that is took to complete the operation", + "network_verification": "Network verification - whether it was used, and what was the result", + "ostf_results": "Is OSTF used, and tests results" + }, + "settings_title": "Environment Settings:", + "settings": { + "envronments_amount": "Number of environments", + "nistribution": "Distribution / OS", + "network_type": "Network type", + "kernel_parameters": "Kernel parameters", + "admin_network_parameters": "Admin network parameters", + "pxe_parameters": "PXE parameters", + "dns_parameters": "DNS parameters", + "storage_options": "Storage options", + "related_projects": "Related Projects", + "modified_settings": "Settings modified on Settings tab", + "networking_configuration": "Networking configuration" + }, + "node_settings_title": "Node Settings:", + "node_settings": { + "deployed_nodes_amount": "Number of nodes deployed", + "deployed_roles": "Roles deployed to each node", + "disk_layout": "Disk layout", + "interfaces_configuration": "Interfaces configuration" + }, + "system_info_title": "System Info:", + "system_info": { + "hypervisor": "Hypervisor", + "hardware_info": "Hardware info", + "fuel_version": "Fuel version info", + "openstack_version": "OpenStack version info" + }, + "setting_labels": { + "send_anonymous_statistic": "Send usage statistics to Mirantis", + "send_user_info": "Identify my error reports so that Mirantis Support can assist me", + "name": "Name", + "email": "Email", + "company": "Company" + }, + "errors": { + "name": "Please fill your name", + "email": "Please fill email address", + "company": "Please fill company name" + } + }, + "welcome_page": { + "title": "Welcome to Mirantis OpenStack", + "support": "Our support team can optionally use your error reports to assist you.", + "provide_contacts": "Please provide your contact information below.", + "start_fuel": "Start Using Fuel", + "change_settings": "You can change your settings at any time by updating your user profile information.", + "thanks": "Thanks for helping to improve Mirantis OpenStack!" + }, "navbar": { "environments": "Environments", "releases": "Releases", @@ -119,7 +183,9 @@ "diagnostic_snapshot": "Diagnostic Snapshot", "capacity_audit": "Capacity Audit", "capacity_audit_text": "To better manage your resources, you can run this report to find out what OpenStack roles have been deployed across all of your environments.", - "view_capacity_audit": "View Capacity Audit" + "view_capacity_audit": "View Capacity Audit", + "send_statistics_title": "Send Statistics About Usage", + "save_changes": "Save Changes" }, "release_page": { "title": "Releases", diff --git a/nailgun/static/js/app.js b/nailgun/static/js/app.js index 7210226d2b..bf4f0e9c6a 100644 --- a/nailgun/static/js/app.js +++ b/nailgun/static/js/app.js @@ -25,6 +25,7 @@ define( 'keystone_client', 'views/common', 'jsx!views/login_page', + 'jsx!views/welcome_page', 'views/cluster_page', 'views/cluster_page_tabs/nodes_tab', 'jsx!views/clusters_page', @@ -33,13 +34,14 @@ define( 'jsx!views/support_page', 'jsx!views/capacity_page' ], -function(React, utils, layoutComponents, Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, ClusterPage, NodesTab, ClustersPage, ReleasesPage, NotificationsPage, SupportPage, CapacityPage) { +function(React, utils, layoutComponents, Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, WelcomePage, ClusterPage, NodesTab, ClustersPage, ReleasesPage, NotificationsPage, SupportPage, CapacityPage) { 'use strict'; var AppRouter = Backbone.Router.extend({ routes: { login: 'login', logout: 'logout', + welcome: 'welcome', clusters: 'listClusters', 'cluster/:id': 'showCluster', 'cluster/:id/:tab(/:opt1)(/:opt2)': 'showClusterTab', @@ -72,8 +74,10 @@ function(React, utils, layoutComponents, Coccyx, coccyxMixins, models, KeystoneC cacheTokenFor: 10 * 60 * 1000, tenant: 'admin' }); - var version = this.version = new models.FuelVersion(); + this.settings = new models.FuelSettings(); + + var version = this.version = new models.FuelVersion(); version.fetch().then(_.bind(function() { this.user = new models.User({authenticated: !version.get('auth_required')}); @@ -179,7 +183,7 @@ function(React, utils, layoutComponents, Coccyx, coccyxMixins, models, KeystoneC this.page = utils.universalMount(new NewPage(options), this.content); this.navbar.setActive(_.result(this.page, 'navbarActiveElement')); this.updateTitle(); - this.toggleElements(NewPage != LoginPage); + this.toggleElements(!this.page.hiddenLayout); }, // routes login: function() { @@ -198,6 +202,9 @@ function(React, utils, layoutComponents, Coccyx, coccyxMixins, models, KeystoneC app.navigate('#login', {trigger: true, replace: true}); }); }, + welcome: function() { + this.setPage(WelcomePage, {settings: this.settings}); + }, showCluster: function(id) { this.navigate('#cluster/' + id + '/nodes', {trigger: true, replace: true}); }, @@ -286,7 +293,7 @@ function(React, utils, layoutComponents, Coccyx, coccyxMixins, models, KeystoneC showSupportPage: function() { var tasks = new models.Tasks(); tasks.fetch().done(_.bind(function() { - this.setPage(SupportPage, {tasks: tasks}); + this.setPage(SupportPage, {tasks: tasks, settings: this.settings}); }, this)); }, showCapacityPage: function() { diff --git a/nailgun/static/js/models.js b/nailgun/static/js/models.js index 4ccf336c86..e341aa50e8 100644 --- a/nailgun/static/js/models.js +++ b/nailgun/static/js/models.js @@ -352,6 +352,70 @@ define(['utils', 'deepModel'], function(utils) { }); _.extend(models.Settings.prototype, cacheMixin); + models.FuelSettings = Backbone.DeepModel.extend({ + constructorName: 'FuelSettings', + url: '/api/settings', + cacheFor: 60 * 1000, + isNew: function() { + return false; + }, + parse: function(response) { + return response.settings; + }, + toJSON: function(options) { + return {settings: this.constructor.__super__.toJSON.call(this, options)}; + }, + expandRestrictions: function(restrictions, path) { + if (_.isUndefined(this.expandedRestrictions)) this.expandedRestrictions = {}; + if (restrictions && restrictions.length) { + var result = _.map(restrictions, utils.expandRestriction, this); + if (path) { + this.expandedRestrictions[path] = result; + } else { + this.expandedRestrictions = result; + } + } + }, + processRestrictions: function() { + _.each(this.attributes, function(group, groupName) { + if (group.metadata) this.expandRestrictions(group.metadata.restrictions, groupName + '.metadata'); + _.each(group, function(setting, settingName) { + this.expandRestrictions(setting.restrictions, this.makePath(groupName, settingName)); + _.each(setting.values, function(value) { + this.expandRestrictions(value.restrictions, this.makePath(groupName, settingName, value.data)); + }, this); + }, this); + }, this); + }, + checkRestrictions: function(models, action, path) { + var restrictions = path ? this.expandedRestrictions[path] : this.expandedRestrictions; + if (action) restrictions = _.where(restrictions, {action: action}); + return _.any(restrictions, function(restriction) { + return utils.evaluateExpression(restriction.condition, models).value; + }); + }, + validate: function(attrs, options) { + var errors = {}, + models = options ? options.models : {}, + checkRestrictions = _.bind(function(path) { + return this.checkRestrictions(models, null, path); + }, this); + _.each(attrs, function(group, groupName) { + if (checkRestrictions(this.makePath(groupName, 'metadata'))) return; + _.each(group, function(setting, settingName) { + var path = this.makePath(groupName, settingName); + if (!setting.regex || !setting.regex.source || checkRestrictions(path)) return; + if (!setting.value.match(new RegExp(setting.regex.source))) errors[path] = setting.regex.error; + }, this); + }, this); + return _.isEmpty(errors) ? null : errors; + }, + makePath: function() { + return _.toArray(arguments).join('.'); + } + }); + _.extend(models.FuelSettings.prototype, cacheMixin); + models.Disk = Backbone.Model.extend({ constructorName: 'Disk', urlRoot: '/api/nodes/', diff --git a/nailgun/static/js/views/controls.jsx b/nailgun/static/js/views/controls.jsx index 0e61ce6b24..4a4054ec7e 100644 --- a/nailgun/static/js/views/controls.jsx +++ b/nailgun/static/js/views/controls.jsx @@ -51,11 +51,13 @@ define(['jquery', 'underscore', 'react'], function($, _, React) { label: React.PropTypes.renderable, description: React.PropTypes.renderable, disabled: React.PropTypes.bool, + inputClassName: React.PropTypes.renderable, wrapperClassName: React.PropTypes.renderable, labelClassName: React.PropTypes.renderable, descriptionClassName: React.PropTypes.renderable, tooltipText: React.PropTypes.renderable, - toggleable: React.PropTypes.bool + toggleable: React.PropTypes.bool, + onChange: React.PropTypes.func }, getInitialState: function() { return {visible: false}; @@ -68,25 +70,28 @@ define(['jquery', 'underscore', 'react'], function($, _, React) { return this.props.type == 'radio' || this.props.type == 'checkbox'; }, onChange: function() { - var input = this.refs.input.getDOMNode(); - return this.props.onChange(this.props.name, this.props.type == 'checkbox' ? input.checked : input.value); + if (this.props.onChange) { + var input = this.refs.input.getDOMNode(); + return this.props.onChange(this.props.name, this.props.type == 'checkbox' ? input.checked : input.value); + } }, renderInput: function() { var input = null, - className = 'parameter-input'; + classes = {'parameter-input': true}; + classes[this.props.inputClassName] = this.props.inputClassName; switch (this.props.type) { case 'select': - input = (); + input = (); break; case 'textarea': - input =