فهرست منبع

Merge pull request #55 from smiclea/CORWEB-18

Refactor and restyle the options page according to design CORWEB-18
Dorin Paslaru 8 سال پیش
والد
کامیت
d8742ca0b8

+ 47 - 12
src/components/AddCloudConnection/AddCloudConnection.js

@@ -24,8 +24,11 @@ import ConnectionsStore from '../../stores/ConnectionsStore';
 import ConnectionsActions from '../../actions/ConnectionsActions';
 import NotificationActions from '../../actions/NotificationActions';
 import Dropdown from '../NewDropdown';
+import Switch from '../Switch'
+import Helper from '../Helper'
 import LoadingIcon from "../LoadingIcon/LoadingIcon";
 import ValidateEndpoint from '../ValidateEndpoint';
+import InfoIcon from '../InfoIcon'
 
 const title = 'Add Cloud Endpoint';
 
@@ -290,13 +293,11 @@ class AddCloudConnection extends Reflux.Component {
     this.state.currentCloud.endpoint.fields.forEach(field => {
       let currentCloudData = this.state.currentCloudData
       switch (field.type) {
-        case 'dropdown':
-          field.options.forEach(option => {
-            if (option.default === true && typeof currentCloudData[field.name] == "undefined") {
-              currentCloudData[field.name] = option.value
-              this.setState({ currentCloudData: currentCloudData })
-            }
-          }, this)
+        case 'switch':
+          if (field.default && typeof currentCloudData[field.name] == "undefined") {
+            currentCloudData[field.name] = field.default
+            this.setState({ currentCloudData: currentCloudData })
+          }
           break
         case 'switch-radio':
           field.options.forEach(option => {
@@ -380,6 +381,8 @@ class AddCloudConnection extends Reflux.Component {
     let currentCloudData = this.state.currentCloudData
     if (field.type == 'dropdown') {
       currentCloudData[field.name] = e.value
+    } else if (field.type === 'switch') {
+      currentCloudData[field.name] = e.target.checked
     } else {
       currentCloudData[field.name] = e.target.value
     }
@@ -435,7 +438,10 @@ class AddCloudConnection extends Reflux.Component {
       case "text":
         returnValue = (
           <div className={"form-group " + (this.isValid(field) ? "" : s.error)} key={"cloudField_" + field.name}>
-            <label>{field.label + (field.required ? " *" : "")}</label>
+            <div className="input-label">
+              {field.label + (field.required ? " *" : "")}
+              <InfoIcon text={Helper.getCloudFieldDescription(field.name)} />
+            </div>
             <input
               type="text"
               placeholder={field.label + (field.required ? " *" : "")}
@@ -448,7 +454,10 @@ class AddCloudConnection extends Reflux.Component {
       case "password":
         returnValue = (
           <div className={"form-group " + (this.isValid(field) ? "" : s.error)} key={"cloudField_" + field.name}>
-            <label>{field.label + (field.required ? " *" : "")}</label>
+            <div className="input-label">
+              {field.label + (field.required ? " *" : "")}
+              <InfoIcon text={Helper.getCloudFieldDescription(field.name)} />
+            </div>
             <input
               type="password"
               placeholder={field.label + (field.required ? " *" : "")}
@@ -458,10 +467,32 @@ class AddCloudConnection extends Reflux.Component {
           </div>
         )
         break;
+      case 'switch':
+        returnValue = (
+          <div
+            className="form-group"
+            key={"cloudField_" + field.name}
+          >
+            <div className="input-label">
+              {field.label + (field.required ? " *" : "")}
+              <InfoIcon text={Helper.getCloudFieldDescription(field.name)} />
+            </div>
+            <Switch
+              checked={this.state.currentCloudData[field.name] === true}
+              onChange={(e) => this.handleCloudFieldChange(e, field)}
+              checkedLabel="Yes"
+              uncheckedLabel="No"
+            />
+          </div>
+        )
+        break
       case "dropdown":
         returnValue = (
           <div className={"form-group " + (this.isValid(field) ? "" : s.error)} key={"cloudField_" + field.name}>
-            <label>{field.label + (field.required ? " *" : "")}</label>
+            <div className="input-label">
+              {field.label + (field.required ? " *" : "")}
+              <InfoIcon text={Helper.getCloudFieldDescription(field.name)} />
+            </div>
             <Dropdown
               options={field.options}
               onChange={(e) => this.handleCloudFieldChange(e, field)}
@@ -524,7 +555,9 @@ class AddCloudConnection extends Reflux.Component {
           <div className={"form-group " + (this.state.cloudFormsSubmitted &&
             this.state.connectionName.trim().length == 0 ? s.error : "")}
           >
-            <label>Endpoint Name *</label>
+            <div className="input-label">
+              Endpoint Name *
+            </div>
             <input
               type="text"
               placeholder="Endpoint Name *"
@@ -533,7 +566,9 @@ class AddCloudConnection extends Reflux.Component {
             />
           </div>
           <div className="form-group">
-            <label>Endpoint Description</label>
+            <div className="input-label">
+              Endpoint Description
+            </div>
             <input
               type="text"
               placeholder="Endpoint Description"

+ 1 - 1
src/components/AddCloudConnection/AddCloudConnection.scss

@@ -47,7 +47,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
       letter-spacing: -0.33px;
       text-transform: uppercase;
     }
-    margin-bottom: 16px;
+    margin-bottom: 32px;
     &:global(.switch-radio) {
       width: 100%;
     }

+ 28 - 1
src/components/App/App.scss

@@ -416,6 +416,25 @@ button {
 :global(.Dropdown-option.is-selected) {
   font-weight: 500;
 }
+
+:global(.reactTooltip) {
+  color: $gray-dark !important;
+  background: $gray-light !important;
+  width: 128px;
+  padding: 8px !important;
+  box-shadow: 0 6px 9px 1px rgba(32, 34, 52, 0.25);
+  margin-left: 12px !important;
+  margin-top: 10px !important;
+  &:after {
+    border-right-color: $gray-light !important;
+    border-right-width: 8px !important;
+    left: -8px !important;
+    border-top-width: 8px !important;
+    border-bottom-width: 8px !important;
+    margin-top: -18px !important;
+  }
+}
+
 :global(.detailViewHead) {
   p {
     margin: 0;
@@ -795,6 +814,14 @@ button {
   }
 }
 
+:global(.input-label) {
+  font-size: 14px;
+  color: $black;
+  margin-bottom: 16px;
+  width: 248px;
+  display: flex;
+}
+
 input[type="text"], input[type="password"] {
   height: 32px;
   border-radius: 4px;
@@ -803,7 +830,7 @@ input[type="text"], input[type="password"] {
   font-size: 14px;
   color: $gray-dark;
   padding: 0 8px 0 16px;
-  font-weight: $weight-regular;
+  font-weight: $weight-light;
   transition: all $animation-swift-out;
   box-sizing: border-box;
   &:hover {

+ 6 - 2
src/components/Helper/Helper.js

@@ -42,8 +42,12 @@ class Helper extends Component {
     return hiddenPass
   }
 
-  static convertCloudFieldLabel(label) {
-    return defaultLabels[label] || label
+  static convertCloudFieldLabel(field) {
+    return (defaultLabels[field] && defaultLabels[field].label) || defaultLabels[field] || field
+  }
+
+  static getCloudFieldDescription(field) {
+    return defaultLabels[field] && defaultLabels[field].description
   }
 
   static convertCloudLabel(label) {

+ 7 - 22
src/components/InfoIcon/InfoIcon.js

@@ -18,38 +18,23 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import React, { Component, PropTypes } from 'react';
 import withStyles from 'isomorphic-style-loader/lib/withStyles';
 import s from './InfoIcon.scss';
-import ReactTooltip from 'react-tooltip'
 
 class InfoIcon extends Component {
   static propTypes = {
     text: PropTypes.string,
-    place: PropTypes.string
-  }
-
-  static defaultProps = {
-    text: "Missing 'text' property",
-    place: "right"
-  }
-
-  componentWillMount() {
-
+    className: PropTypes.string
   }
 
   render() {
+    let className = s.root + ' ' + (this.props.className || '')
+    let icon = this.props.text ? <div className={s.icon} data-tip={this.props.text} /> : null
+
     return (
-      <div data-tip={this.props.text} className={s.root}>
-        <ReactTooltip
-          className={s.infoIcon}
-          multiline={true} // eslint-disable-line react/jsx-boolean-value
-          type="light"
-          place={this.props.place}
-          effect="solid"
-          border={true} // eslint-disable-line react/jsx-boolean-value
-        />
+      <div className={className}>
+        {icon}
       </div>
-    );
+    )
   }
-
 }
 
 export default withStyles(InfoIcon, s);

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
src/components/InfoIcon/InfoIcon.scss


+ 5 - 5
src/components/InfoIcon/package.json

@@ -1,6 +1,6 @@
 {
-  "name": "InfoIcon",
-  "version": "0.0.0",
-  "private": true,
-  "main": "./InfoIcon.js"
-}
+    "name": "InfoIcon",
+    "version": "0.0.0",
+    "private": true,
+    "main": "./InfoIcon.js"
+}

+ 74 - 0
src/components/Switch/Switch.js

@@ -0,0 +1,74 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React, { Component, PropTypes } from 'react';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import s from './Switch.scss';
+
+class Switch extends Component {
+  static propTypes = {
+    onChange: PropTypes.func,
+    checked: PropTypes.bool,
+    checkedLabel: PropTypes.string,
+    uncheckedLabel: PropTypes.string
+  }
+
+  constructor(props) {
+    super(props)
+
+    this.state = {
+      checked: props && props.checked
+    }
+  }
+
+  handleChange(e) {
+    if (this.props.onChange) {
+      this.props.onChange(e)
+    }
+
+    this.setState({ checked: e.target.checked })
+  }
+
+  render() {
+    let renderLabel = () => {
+      if ((this.state.checked && this.props.checkedLabel) || (!this.state.checked && this.props.uncheckedLabel)) {
+        return (
+          <div className={s.label}>
+            {this.state.checked ? this.props.checkedLabel : this.props.uncheckedLabel}
+          </div>
+        )
+      }
+      return null
+    }
+
+    return (
+      <div className={s.root}>
+        <input
+          type="checkbox"
+          className={s.input}
+          checked={this.state.checked}
+          className="ios-switch tinyswitch"
+          onChange={this.handleChange.bind(this)}
+        />
+        <div><div></div></div>
+        {renderLabel()}
+      </div>
+    )
+  }
+}
+
+export default withStyles(Switch, s);

+ 66 - 0
src/components/Switch/Switch.scss

@@ -0,0 +1,66 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import '../variables.scss';
+
+.root {
+  display: flex;
+  align-items: center;
+  position: relative;
+
+    .label {
+      margin-left: 16px;
+      color: $gray-dark;
+      font-weight: $weight-light;
+    }
+
+    input[type="checkbox"] {
+      width: 32px;
+      height: 16px;
+      cursor: pointer;
+    }
+
+    input[type="checkbox"]:global(.tinyswitch.ios-switch) + div {
+      width: 30px;	
+      height: 14px;
+      background-color: $task-gray-light;
+      border: 1px solid $task-gray-light;
+    }
+
+    input[type="checkbox"]:global(.ios-switch):checked + div {
+      background-color: $blue;
+      border: 1px solid $blue;
+      box-shadow: none;
+    }
+
+    input[type="checkbox"]:global(.ios-switch) + div { 
+      transition-duration: 0.2s;
+    }
+    
+    input[type="checkbox"]:global(.ios-switch) + div > div {
+      transition-timing-function: ease;
+      transition-duration: 0.2s;
+    }
+
+    input[type="checkbox"]:global(.tinyswitch.ios-switch) + div > div {
+      height: 14px;
+      width: 14px;
+      margin-left: 0px;
+      margin-top: 0px;
+      box-shadow: none;
+    }
+}

+ 6 - 0
src/components/Switch/package.json

@@ -0,0 +1,6 @@
+{
+    "name": "Switch",
+    "version": "0.0.0",
+    "private": true,
+    "main": "./Switch.js"
+}

+ 60 - 13
src/components/WizardOptions/WizardOptions.js

@@ -16,14 +16,17 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 import React, { PropTypes } from 'react';
+import ReactTooltip from 'react-tooltip'
 import Reflux from 'reflux';
 import withStyles from 'isomorphic-style-loader/lib/withStyles';
 import s from './WizardOptions.scss';
 import Dropdown from '../NewDropdown';
+import Helper from '../Helper'
 import WizardActions from '../../actions/WizardActions';
 import WizardStore from '../../stores/WizardStore';
 import NotificationActions from '../../actions/NotificationActions';
-
+import Switch from '../Switch'
+import InfoIcon from '../InfoIcon'
 
 const title = 'Migration Options';
 
@@ -121,10 +124,17 @@ class WizardOptions extends Reflux.Component {
     WizardActions.updateWizardState({ showAdvancedOptions: showAdvancedOptions })
   }
 
+  handleAdvancedOptionsSwitch(e) {
+    this.setState({ showAdvancedOptions: e.target.checked })
+    WizardActions.updateWizardState({ showAdvancedOptions: e.target.checked })
+  }
+
   handleOptionsFieldChange(e, field) {
     let destinationEnvironment = this.state.destination_environment
-    if (field.type == 'dropdown') {
+    if (field.type === 'dropdown') {
       destinationEnvironment[field.name] = e.value
+    } else if (field.type === 'switch') {
+      destinationEnvironment[field.name] = e.target.checked
     } else {
       destinationEnvironment[field.name] = e.target.value
     }
@@ -150,7 +160,10 @@ class WizardOptions extends Reflux.Component {
             className={"form-group " + extraClasses}
             key={"cloudField_" + field.name}
           >
-            <h3>{field.label + (field.required ? " *" : "")}</h3>
+            <div className="input-label">
+              {field.label + (field.required ? " *" : "")}
+              <InfoIcon text={Helper.getCloudFieldDescription(field.name)} />
+            </div>
             <input
               type="text"
               placeholder={field.label + (field.required ? " *" : "")}
@@ -166,7 +179,10 @@ class WizardOptions extends Reflux.Component {
             className={"form-group " + extraClasses}
             key={"cloudField_" + field.name}
           >
-            <h3>{field.label + (field.required ? " *" : "")}</h3>
+            <div className="input-label">{
+              field.label + (field.required ? " *" : "")}
+              <InfoIcon text={Helper.getCloudFieldDescription(field.name)} />
+            </div>
             <input
               type="password"
               placeholder={field.label + (field.required ? " *" : "")}
@@ -176,6 +192,25 @@ class WizardOptions extends Reflux.Component {
           </div>
         )
         break;
+      case 'switch':
+        returnValue = (
+          <div
+            className={"form-group " + extraClasses}
+            key={"cloudField_" + field.name}
+          >
+            <div className="input-label">
+              {field.label + (field.required ? " *" : "")}
+              <InfoIcon text={Helper.getCloudFieldDescription(field.name)} />
+            </div>
+            <Switch
+              checked={this.state.destination_environment[field.name] === true}
+              onChange={(e) => this.handleOptionsFieldChange(e, field)}
+              checkedLabel="Yes"
+              uncheckedLabel="No"
+            />
+          </div>
+        )
+        break
       case "dropdown":
         let value = this.state.destination_environment[field.name]
 
@@ -188,7 +223,10 @@ class WizardOptions extends Reflux.Component {
             className={"form-group " + extraClasses}
             key={"cloudField_" + field.name}
           >
-            <h3>{field.label + (field.required ? " *" : "")}</h3>
+            <div className="input-label">
+              {field.label + (field.required ? " *" : "")}
+              <InfoIcon text={Helper.getCloudFieldDescription(field.name)} />
+            </div>
             <Dropdown
               options={field.options}
               onChange={(e) => this.handleOptionsFieldChange(e, field)}
@@ -239,8 +277,12 @@ class WizardOptions extends Reflux.Component {
     }
     if (!this.state.isConnecting) {
       let optionFields = fields.map(field => this.renderField(field), this)
+      let totalFields = optionFields.filter(f => f !== undefined).length
+      let requiredFields = fields.filter(f => f.required).length
+      let visibleFields = this.state.showAdvancedOptions ? totalFields : requiredFields
+
       return (
-        <div className={s.optionsFieldsContainer}>
+        <div className={s.optionsFieldsContainer + ' ' + (visibleFields <= 4 ? s.oneColumn : '')}>
           {optionFields}
         </div>
       )
@@ -254,21 +296,26 @@ class WizardOptions extends Reflux.Component {
   }
 
   render() {
-    let toggleAdvancedBtn = (<button
-      onClick={(e) => this.toggleAdvancedOptions(e)}
-      className={s.toggleAdvancedBtn + " wire"}
-    >
-      {this.state.showAdvancedOptions ? "Hide" : "Show"} Advanced Options
-    </button>)
+    let toggleAdvancedSwitch = (
+      <div className={s.toggleAdvancedSwitch}>
+        <div className={s.toggleAdvancedLabel}>Simple</div>
+        <Switch
+          checked={this.state.showAdvancedOptions}
+          onChange={this.handleAdvancedOptionsSwitch.bind(this)}
+        />
+        <div className={s.toggleAdvancedLabel}>Advanced</div>
+      </div>
+    )
 
     return (
       <div className={s.root}>
         <div className={s.container}>
+          {toggleAdvancedSwitch}
           <div className={s.containerCenter}>
             {this.renderOptionsFields(this.state.targetCloud.cloudRef["import_" + this.state.migrationType].fields)}
           </div>
-          {toggleAdvancedBtn}
         </div>
+        <ReactTooltip place="right" effect="solid" className="reactTooltip" />
       </div>
     );
   }

+ 26 - 35
src/components/WizardOptions/WizardOptions.scss

@@ -50,46 +50,42 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     margin-left: 16px;
   }
 }
-.containerLeft {
-  width: 272px;
-  float: left;
-}
 .containerCenter {
-  width: 800px;
+  width: $wizard-content-width;
   margin: 0 auto;
 }
-.containerRight {
-  width: 272px;
-  float: right;
-  :global(.Dropdown-root) {
-    width: 100%;
-  }
-}
-.containerRight, .containerLeft {
-  &:after {
-    display: block;
-    height: 0;
-    clear: both;
-    content: " ";
-  }
-}
 
-.toggleAdvancedBtn {
-  clear: both;
-  width: 240px;
-  margin: 24px auto 0;
-  display: block;
+.toggleAdvancedSwitch {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 47px;
+
+  .toggleAdvancedLabel {
+    font-weight: $weight-light;
+    color: $gray-dark;
+    margin: 0 16px;
+  }
 }
 
 .optionsFieldsContainer {
+  display: flex;
+  flex-wrap: wrap;
+  margin-left: -304px;
+
+  &.oneColumn {
+    align-items: center;
+    flex-direction: column;
+  }
+
   :global {
     .form-group {
-      float: left;
-      width: 40%;
       display: none;
-      &:nth-child(2n) {
-        float: right
-      }
+      min-width: 208px;
+      max-width: 208px;
+      flex-grow: 1;
+      margin-left: 304px;
+      margin-bottom: 32px;
       &.required, &.showAdvanced {
         display: block;
       }
@@ -103,9 +99,4 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
       }
     }
   }
-  &:after {
-    content: " ";
-    display: block;
-    clear: both;
-  }
 }

+ 1 - 13
src/components/WizardSummary/WizardSummary.js

@@ -61,24 +61,12 @@ class WizardSummary extends Component {
     return fields.map(field => (
       <div className={s.row} key={"destination_environment_" + field.label}>
         <span>{field.label}</span>
-        <span>{field.value}</span>
+        <span>{field.value.toString()}</span>
       </div>
     ))
   }
 
   render() {
-    let schedules = this.props.summary.schedules.map((item, index) => (
-        <div className="item" key={"schedule_" + index}>
-          <span className="cell">
-            {this.dateTypes.indexOf(item.type) == -1 ? item.type : moment(item.date).format("MMM Do YYYY")}
-          </span>
-          <span className="cell">
-            {item.type != "Execute Now" &&
-              (item.hour.label + ":" + item.minute.label + " " + item.tod + " " + item.timezone)}
-          </span>
-        </div>
-      ), this)
-
     let instances = this.props.summary.selectedInstances.map((vm, index) => (
       <div className="item" key={"VM_" + index}>
         <span className="cell">

+ 1 - 1
src/constants/CloudLabels.js

@@ -26,7 +26,7 @@ export const defaultLabels = {
   user_domain_name: "User Domain Name",
   project_name: "Project Name",
   project_domain_name: "Project Domain Name",
-  flavor_name: "Flavor Name",
+  flavor_name: { label: "Flavor Name", description: "Let Coriolis store a flavor name" },
   hypervisor_type: "Hypervisor Type",
   container_format: "Container Format",
   disk_format: "Disk Format",

+ 1 - 7
src/stores/ConnectionsStore/ConnectionsStore.js

@@ -271,13 +271,7 @@ class ConnectionsStore extends Reflux.Store
       }
       switch (cloudData.properties[propName].type) {
         case "boolean":
-          // Values need to be strings, due to a limitation in react-dropdown
-          field.options = [
-            {label: 'Yes', value: 'true'}, 
-            {label: 'No', value: 'false'}
-          ]
-
-          field.default = (field.default === 'true' || field.default === true) ? 'true' : 'false'
+          field.type = "switch"
           break
 
         case "string":

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است