Просмотр исходного кода

Merge pull request #97 from smiclea/CORWEB-147

Add tooltip to Wizard's options CORWEB-147
Dorin Paslaru 8 лет назад
Родитель
Сommit
4506807ba0

+ 3 - 2
package.json

@@ -66,11 +66,11 @@
     "history": "^4.7.2",
     "html-webpack-plugin": "^2.30.1",
     "js-cookie": "^2.1.4",
-    "path": "^0.12.7",
-    "raw-loader": "^0.5.1",
     "lodash": "^4.17.4",
     "moment": "^2.18.1",
+    "path": "^0.12.7",
     "prop-types": "^15.6.0",
+    "raw-loader": "^0.5.1",
     "react": "^16.0.0",
     "react-collapse": "^4.0.3",
     "react-datetime": "^2.10.3",
@@ -81,6 +81,7 @@
     "react-motion": "^0.5.2",
     "react-notification-system": "^0.2.15",
     "react-router-dom": "^4.2.2",
+    "react-tooltip": "^3.4.0",
     "rimraf": "^2.6.2",
     "styled-components": "2.2.0",
     "styled-tools": "^0.2.2",

+ 43 - 0
src/components/atoms/InfoIcon/InfoIcon.jsx

@@ -0,0 +1,43 @@
+/*
+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 from 'react'
+import styled from 'styled-components'
+import PropTypes from 'prop-types'
+
+import questionImage from './images/question.svg'
+
+const Wrapper = styled.div`
+  width: 16px;
+  height: 16px;
+  background: url('${questionImage}') center no-repeat;
+  display: inline-block;
+  margin-bottom: -4px;
+  margin-left: ${props => props.marginLeft ? `${props.marginLeft}px` : '4px'};
+`
+
+class InfoIcon extends React.Component {
+  static propTypes = {
+    text: PropTypes.string,
+    marginLeft: PropTypes.number,
+  }
+
+  render() {
+    return (
+      <Wrapper data-tip={this.props.text} marginLeft={this.props.marginLeft} />
+    )
+  }
+}
+
+export default InfoIcon

Разница между файлами не показана из-за своего большого размера
+ 13 - 0
src/components/atoms/InfoIcon/images/question.svg


+ 10 - 1
src/components/atoms/Switch/Switch.jsx

@@ -24,6 +24,8 @@ const Wrapper = styled.div`
   height: ${StyleProps.inputSizes.regular.height}px;
   align-items: center;
   ${props => props.disabled ? 'opacity: 0.5;' : ''}
+  ${props => props.justifyContent ? `justify-content: ${props.justifyContent};` : ''}
+  ${props => props.width ? `width: ${props.width};` : ''}
 `
 const InputWrapper = styled.div`
   position: relative;
@@ -118,6 +120,8 @@ class Switch extends React.Component {
     secondary: PropTypes.bool,
     noLabel: PropTypes.bool,
     height: PropTypes.number,
+    width: PropTypes.string,
+    justifyContent: PropTypes.string,
     big: PropTypes.bool,
     checkedLabel: PropTypes.string,
     uncheckedLabel: PropTypes.string,
@@ -215,7 +219,12 @@ class Switch extends React.Component {
 
   render() {
     return (
-      <Wrapper disabled={this.props.disabled} style={this.props.style}>
+      <Wrapper
+        disabled={this.props.disabled}
+        style={this.props.style}
+        width={this.props.width}
+        justifyContent={this.props.justifyContent}
+      >
         {this.renderLeftLabel()}
         {this.renderInput()}
         {this.renderRightLabel()}

+ 53 - 0
src/components/atoms/Tooltip/Tooltip.jsx

@@ -0,0 +1,53 @@
+/*
+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 from 'react'
+import { injectGlobal } from 'styled-components'
+import ReactTooltip from 'react-tooltip'
+
+import Palette from '../../styleUtils/Palette'
+
+injectGlobal`
+  .reactTooltip {
+    color: ${Palette.grayscale[4]} !important;
+    background: ${Palette.grayscale[1]} !important;
+    max-width: 192px;
+    padding: 8px !important;
+    box-shadow: 0 0 9px 1px rgba(32, 34, 52, 0.1);
+    margin-left: 12px !important;
+    opacity: 1 !important;
+    &:after {
+      border-right-color: ${Palette.grayscale[1]} !important;
+      border-right-width: 8px !important;
+      left: -8px !important;
+      border-top-width: 8px !important;
+      border-bottom-width: 8px !important;
+      margin-top: -8px !important;
+    }
+  }
+`
+
+class Tooltip extends React.Component {
+  static rebuild = () => {
+    ReactTooltip.rebuild()
+  }
+
+  render() {
+    return (
+      <ReactTooltip place="right" effect="solid" className="reactTooltip" />
+    )
+  }
+}
+
+export default Tooltip

+ 32 - 0
src/components/atoms/Tooltip/story.jsx

@@ -0,0 +1,32 @@
+/*
+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 from 'react'
+import { storiesOf } from '@storybook/react'
+import { WizardOptionsField } from 'components'
+
+import Tooltip from './Tooltip'
+
+storiesOf('Tooltip', module)
+  .add('default', () => (
+    <div>
+      <WizardOptionsField
+        name="separate_vm"
+        type="boolean"
+        value
+        onChange={() => { }}
+      />
+      <Tooltip />
+    </div>
+  ))

+ 29 - 6
src/components/molecules/Field/Field.jsx → src/components/molecules/EndpointField/EndpointField.jsx

@@ -16,8 +16,9 @@ import React from 'react'
 import styled from 'styled-components'
 import PropTypes from 'prop-types'
 
-import { Switch, TextInput, Dropdown, RadioInput } from 'components'
+import { Switch, TextInput, Dropdown, RadioInput, InfoIcon } from 'components'
 
+import LabelDictionary from '../../../utils/LabelDictionary'
 import StyleProps from '../../styleUtils/StyleProps'
 import Palette from '../../styleUtils/Palette'
 
@@ -29,11 +30,14 @@ const Label = styled.div`
   text-transform: uppercase;
   margin-bottom: 4px;
 `
+const LabelText = styled.span`
+  margin-right: 24px;
+`
 
 class Field extends React.Component {
   static propTypes = {
-    label: PropTypes.string.isRequired,
-    type: PropTypes.string.isRequired,
+    name: PropTypes.string,
+    type: PropTypes.string,
     value: PropTypes.any,
     onChange: PropTypes.func,
     className: PropTypes.string,
@@ -65,7 +69,7 @@ class Field extends React.Component {
         large={this.props.large}
         value={this.props.value}
         onChange={e => { this.props.onChange(e.target.value) }}
-        placeholder={this.props.label}
+        placeholder={LabelDictionary.get(this.props.name)}
         disabled={this.props.disabled}
       />
     )
@@ -96,7 +100,7 @@ class Field extends React.Component {
     return (
       <RadioInput
         checked={this.props.value}
-        label={this.props.label}
+        label={LabelDictionary.get(this.props.name)}
         onChange={e => this.props.onChange(e.target.checked)}
         disabled={this.props.disabled}
       />
@@ -121,10 +125,29 @@ class Field extends React.Component {
     }
   }
 
+  renderLabel() {
+    if (this.props.type === 'radio') {
+      return null
+    }
+
+    let description = LabelDictionary.getDescription(this.props.name)
+    let infoIcon = null
+    if (description) {
+      infoIcon = <InfoIcon text={description} marginLeft={-20} />
+    }
+
+    return (
+      <Label>
+        <LabelText>{LabelDictionary.get(this.props.name)}</LabelText>
+        {infoIcon}
+      </Label>
+    )
+  }
+
   render() {
     return (
       <Wrapper className={this.props.className}>
-        {this.props.type !== 'radio' ? <Label>{this.props.label}</Label> : null}
+        {this.renderLabel()}
         {this.renderInput()}
       </Wrapper>
     )

+ 14 - 14
src/components/molecules/Field/story.jsx → src/components/molecules/EndpointField/story.jsx

@@ -14,7 +14,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from 'react'
 import { storiesOf } from '@storybook/react'
-import Field from './Field'
+import EndpointField from './EndpointField'
 
 class Wrapper extends React.Component {
   constructor() {
@@ -28,7 +28,7 @@ class Wrapper extends React.Component {
 
   render() {
     return (
-      <Field
+      <EndpointField
         {...this.props}
         value={this.state.value}
         onChange={value => { this.handleChange(value) }}
@@ -37,37 +37,37 @@ class Wrapper extends React.Component {
   }
 }
 
-storiesOf('Field', module)
+storiesOf('EndpointField', module)
   .add('text input', () => (
-    <Wrapper label="Field label" type="string" />
+    <Wrapper name="field_name" type="string" />
   ))
   .add('text input large', () => (
-    <Wrapper large label="Field label" type="string" />
+    <Wrapper large name="field_name" type="string" />
   ))
   .add('text input disabled', () => (
-    <Wrapper label="Field label" type="string" disabled />
+    <Wrapper name="field_name" type="string" disabled />
   ))
   .add('text input highlight', () => (
-    <Wrapper label="Field label" type="string" highlight />
+    <Wrapper name="field_name" type="string" highlight />
   ))
   .add('text input password', () => (
-    <Wrapper label="Field label" type="string" password />
+    <Wrapper name="field_name" type="string" password />
   ))
   .add('switch', () => (
-    <Wrapper label="Field label" type="boolean" />
+    <Wrapper name="migr_worker_use_config_drive" type="boolean" />
   ))
   .add('number dropdown', () => (
-    <Wrapper label="Field label" type="integer" minimum={1} maximum={5} />
+    <Wrapper name="field_name" type="integer" minimum={1} maximum={5} />
   ))
   .add('number dropdown large', () => (
-    <Wrapper label="Field label" type="integer" minimum={1} maximum={5} large />
+    <Wrapper name="field_name" type="integer" minimum={1} maximum={5} large />
   ))
   .add('number dropdown disabled', () => (
-    <Wrapper label="Field label" type="integer" minimum={1} maximum={5} disabled />
+    <Wrapper name="field_name" type="integer" minimum={1} maximum={5} disabled />
   ))
   .add('radio', () => (
-    <Wrapper label="Field label" type="radio" />
+    <Wrapper name="field_name" type="radio" />
   ))
   .add('radio disabled', () => (
-    <Wrapper label="Field label" type="radio" disabled />
+    <Wrapper name="field_name" type="radio" disabled />
   ))

+ 9 - 9
src/components/molecules/Field/test.jsx → src/components/molecules/EndpointField/test.jsx

@@ -14,24 +14,24 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from 'react'
 import { shallow } from 'enzyme'
-import Field from './Field'
+import EndpointField from './EndpointField'
 
-const wrap = props => shallow(<Field {...props} />)
+const wrap = props => shallow(<EndpointField {...props} />)
 
 it('renders boolean field with correct value', () => {
-  let wrapper = wrap({ type: 'boolean', label: 'label', value: true })
+  let wrapper = wrap({ type: 'boolean', name: 'label', value: true })
   expect(wrapper.childAt(1).name()).toBe('Switch')
   expect(wrapper.childAt(1).prop('checked')).toBe(true)
 })
 
 it('renders boolean field disabled', () => {
-  let wrapper = wrap({ type: 'boolean', label: 'label', disabled: true })
+  let wrapper = wrap({ type: 'boolean', name: 'label', disabled: true })
   expect(wrapper.childAt(1).prop('disabled')).toBe(true)
 })
 
 it('renders text input field with correct label and value', () => {
-  let wrapper = wrap({ type: 'string', label: 'field-label', value: 'text-input' })
-  expect(wrapper.childAt(0).contains('field-label')).toBe(true)
+  let wrapper = wrap({ type: 'string', name: 'field_label', value: 'text-input' })
+  expect(wrapper.childAt(0).contains('Field Label')).toBe(true)
   expect(wrapper.childAt(1).name()).toBe('TextInput')
   expect(wrapper.childAt(1).prop('value')).toBe('text-input')
 })
@@ -39,7 +39,7 @@ it('renders text input field with correct label and value', () => {
 it('renders text input field with password, large, disabled, highlighted and required', () => {
   let wrapper = wrap({
     type: 'string',
-    label: 'field-label',
+    name: 'field-label',
     value: 'text-input',
     password: true,
     large: true,
@@ -57,7 +57,7 @@ it('renders text input field with password, large, disabled, highlighted and req
 it('renders integer dropdown field with correct items', () => {
   let wrapper = wrap({
     type: 'integer',
-    label: 'field-label',
+    name: 'field-label',
     value: 11,
     minimum: 10,
     maximum: 15,
@@ -68,7 +68,7 @@ it('renders integer dropdown field with correct items', () => {
 })
 
 it('renders radio input field with correct value', () => {
-  let wrapper = wrap({ type: 'radio', label: 'label', value: true })
+  let wrapper = wrap({ type: 'radio', name: 'label', value: true })
   expect(wrapper.childAt(0).name()).toBe('RadioInput')
   expect(wrapper.childAt(0).prop('checked')).toBe(true)
 })

+ 18 - 2
src/components/molecules/WizardOptionsField/WizardOptionsField.jsx

@@ -16,7 +16,7 @@ import React from 'react'
 import styled from 'styled-components'
 import PropTypes from 'prop-types'
 
-import { Switch, TextInput, PropertiesTable, Dropdown } from 'components'
+import { Switch, TextInput, PropertiesTable, Dropdown, InfoIcon } from 'components'
 
 import StyleProps from '../../styleUtils/StyleProps'
 import LabelDictionary from '../../../utils/LabelDictionary'
@@ -37,6 +37,9 @@ const Label = styled.div`
   font-weight: ${StyleProps.fontWeights.medium};
   ${props => getDirection(props) === 'column' ? 'margin-bottom: 8px;' : ''}
 `
+const LabelText = styled.span`
+  margin-right: 24px;
+`
 
 class WizardOptionsField extends React.Component {
   static propTypes = {
@@ -54,6 +57,8 @@ class WizardOptionsField extends React.Component {
   renderSwitch({ triState }) {
     return (
       <Switch
+        width="112px"
+        justifyContent="flex-end"
         triState={triState}
         checked={this.props.value}
         onChange={checked => { this.props.onChange(checked) }}
@@ -141,7 +146,18 @@ class WizardOptionsField extends React.Component {
   }
 
   renderLabel() {
-    return <Label>{LabelDictionary.get(this.props.name)}</Label>
+    let description = LabelDictionary.getDescription(this.props.name)
+    let infoIcon = null
+    if (description) {
+      infoIcon = <InfoIcon text={description} marginLeft={-20} />
+    }
+
+    return (
+      <Label>
+        <LabelText>{LabelDictionary.get(this.props.name)}</LabelText>
+        {infoIcon}
+      </Label>
+    )
   }
 
   render() {

+ 5 - 4
src/components/organisms/Endpoint/Endpoint.jsx

@@ -17,8 +17,7 @@ import styled from 'styled-components'
 import PropTypes from 'prop-types'
 import connectToStores from 'alt-utils/lib/connectToStores'
 
-import { EndpointLogos, Field, Button, StatusIcon, LoadingButton, CopyButton } from 'components'
-import LabelDictionary from '../../../utils/LabelDictionary'
+import { EndpointLogos, EndpointField, Button, StatusIcon, LoadingButton, CopyButton, Tooltip } from 'components'
 import NotificationActions from '../../../actions/NotificationActions'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointActions from '../../../actions/EndpointActions'
@@ -41,7 +40,7 @@ const Fields = styled.div`
   margin-left: -64px;
   margin-top: 32px;
 `
-const FieldStyled = styled(Field) `
+const FieldStyled = styled(EndpointField)`
   margin-left: 64px;
   min-width: 224px;
   max-width: 224px;
@@ -334,7 +333,7 @@ class Endpoint extends React.Component {
           password={field.name === 'password'}
           type={field.type}
           highlight={this.state.invalidFields.findIndex(fn => fn === field.name) > -1}
-          label={LabelDictionary.get(field.name)}
+          name={field.name}
           value={this.getFieldValue(field, parentGroup)}
           onChange={value => { this.handleFieldChange(field, value, parentGroup) }}
         />
@@ -415,6 +414,8 @@ class Endpoint extends React.Component {
         {this.renderEndpointStatus()}
         <Fields>
           {this.renderFields(this.props.providerStore.connectionInfoSchema)}
+          <Tooltip />
+          {Tooltip.rebuild()}
         </Fields>
         <Buttons>
           <Button large secondary onClick={() => { this.handleCancelClick() }}>Cancel</Button>

+ 3 - 1
src/components/organisms/WizardOptions/WizardOptions.jsx

@@ -16,7 +16,7 @@ import React from 'react'
 import styled from 'styled-components'
 import PropTypes from 'prop-types'
 
-import { ToggleButtonBar, WizardOptionsField } from 'components'
+import { ToggleButtonBar, WizardOptionsField, Tooltip } from 'components'
 
 import { executionOptions } from '../../../config'
 
@@ -142,6 +142,7 @@ class WizardOptions extends React.Component {
           <OneColumn>
             {fields.map(f => f.component)}
           </OneColumn>
+          <Tooltip />
         </Fields>
       )
     }
@@ -154,6 +155,7 @@ class WizardOptions extends React.Component {
         <Column right>
           {fields.map(f => f.column === 'right' && f.component)}
         </Column>
+        <Tooltip />
       </Fields>
     )
   }

+ 10 - 2
src/components/organisms/WizardOptions/story.jsx

@@ -18,8 +18,16 @@ import WizardOptions from './WizardOptions'
 
 let fields = [
   {
-    name: 'string_field',
-    type: 'string',
+    name: 'list_all_destination_networks',
+    type: 'boolean',
+  },
+  {
+    name: 'migr_worker_use_config_drive',
+    type: 'boolean',
+  },
+  {
+    name: 'set_dhcp',
+    type: 'boolean',
   },
   {
     name: 'string_field_with_default',

+ 19 - 4
src/utils/LabelDictionary.js

@@ -86,25 +86,40 @@ class LabelDictionary {
     force: 'Force',
     skip_os_morphing: 'Skip OS Morphing',
     shutdown_instances: 'Shutdown Instances',
-    separate_vm: 'Separate Migration/VM?',
     aws: 'Amazon',
     openstack: 'OpenStack',
     oracle_vm: 'Oracle VM',
     opc: 'Oracle Cloud',
     azure: 'Azure',
     vmware_vsphere: 'VMware',
+    separate_vm: { label: 'Separate Migration/VM?', description: 'Separate migration per selected instance' },
   }
 
   static get(fieldName) {
-    let label = this.dictionary[fieldName]
-    if (label) {
-      return label
+    let labelInfo = this.dictionary[fieldName]
+    if (labelInfo) {
+      if (typeof labelInfo === 'string') {
+        return labelInfo
+      }
+      if (labelInfo.label) {
+        return labelInfo.label
+      }
     }
 
     let words = fieldName.split('_')
     words = words.map(word => word.charAt(0).toUpperCase() + word.substr(1))
     return words.join(' ')
   }
+
+  static getDescription(fieldName) {
+    let labelInfo = this.dictionary[fieldName]
+
+    if (labelInfo && typeof labelInfo === 'object') {
+      return labelInfo.description || ''
+    }
+
+    return ''
+  }
 }
 
 export default LabelDictionary

+ 7 - 10
yarn.lock

@@ -4033,10 +4033,6 @@ is-windows@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9"
 
-is-wsl@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
-
 isarray@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -5180,12 +5176,6 @@ onetime@^2.0.0:
   dependencies:
     mimic-fn "^1.0.0"
 
-opn@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/opn/-/opn-5.1.0.tgz#72ce2306a17dbea58ff1041853352b4a8fc77519"
-  dependencies:
-    is-wsl "^1.1.0"
-
 optimist@^0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@@ -6118,6 +6108,13 @@ react-test-renderer@^16.0.0-0:
     object-assign "^4.1.1"
     prop-types "^15.6.0"
 
+react-tooltip@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-3.4.0.tgz#037f38f797c3e6b1b58d2534ccc8c2c76af4f52d"
+  dependencies:
+    classnames "^2.2.5"
+    prop-types "^15.6.0"
+
 react-transition-group@^1.1.2:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6"

Некоторые файлы не были показаны из-за большого количества измененных файлов