Jelajahi Sumber

Merge pull request #127 from smiclea/CORWEB-163

Create Endpoint plugin system CORWEB-163
Dorin Paslaru 8 tahun lalu
induk
melakukan
2b18de26a6

+ 1 - 1
src/actions/EndpointActions.js

@@ -124,7 +124,7 @@ class EndpointActions {
   }
 
   addFailed(response) {
-    return response || true
+    throw response
   }
 }
 

+ 4 - 0
src/actions/ProviderActions.js

@@ -33,6 +33,10 @@ class ProviderActions {
     return response || true
   }
 
+  clearConnectionInfoSchema() {
+    return true
+  }
+
   loadProviders() {
     ProviderSource.loadProviders().then(
       providers => { this.loadProvidersSuccess(providers) },

+ 2 - 2
src/components/atoms/DropdownButton/DropdownButton.jsx

@@ -44,7 +44,7 @@ const Label = styled.div`
 
 const getBackgroundColor = props => {
   if (props.disabled) {
-    return Palette.grayscale[7]
+    return Palette.grayscale[0]
   }
 
   if (props.primary) {
@@ -75,7 +75,7 @@ const getWidth = props => {
 }
 const borderColor = props => {
   if (props.disabled) {
-    return Palette.grayscale[7]
+    return Palette.grayscale[0]
   }
   if (props.primary) {
     return Palette.primary

+ 69 - 0
src/components/atoms/TextArea/TextArea.jsx

@@ -0,0 +1,69 @@
+/*
+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 Palette from '../../styleUtils/Palette'
+import StyleProps from '../../styleUtils/StyleProps'
+
+const getInputWidth = props => {
+  if (props.width) {
+    return props.width
+  }
+
+  if (props.large) {
+    return `${StyleProps.inputSizes.large.width}px`
+  }
+
+  return `${StyleProps.inputSizes.regular.width}px`
+}
+
+const Input = styled.textarea`
+  width: ${props => getInputWidth(props)};
+  height: ${props => props.height || `${StyleProps.inputSizes.regular.height * 2}px`};
+  border-radius: ${StyleProps.borderRadius};
+  background-color: #FFF;
+  border: 1px solid ${props => props.highlight ? Palette.alert : Palette.grayscale[3]};
+  color: ${Palette.black};
+  padding: 8px;
+  font-size: inherit;
+  transition: all ${StyleProps.animations.swift};
+  box-sizing: border-box;
+  font-family: monospace;
+  &:hover {
+    border-color: ${props => props.highlight ? Palette.alert : Palette.primary};
+  }
+  &:focus {
+    border-color: ${props => props.highlight ? Palette.alert : Palette.primary};
+    outline: none;
+  }
+  &:disabled {
+    color: ${Palette.grayscale[3]};
+    border-color: ${Palette.grayscale[0]};
+    background-color: ${Palette.grayscale[0]};
+  }
+  &::placeholder {
+    color: ${Palette.grayscale[3]};
+  }
+`
+
+class TextArea extends React.Component {
+  render() {
+    return (
+      <Input {...this.props} />
+    )
+  }
+}
+
+export default TextArea

+ 12 - 2
src/components/molecules/Dropdown/Dropdown.jsx

@@ -145,7 +145,17 @@ class Dropdown extends React.Component {
     let buttonHeight = this.buttonRef.offsetHeight
     let tipHeight = 8
     let buttonOffset = offset(this.buttonRef)
-    this.listRef.style.top = `${buttonOffset.top + buttonHeight + tipHeight}px`
+    let listTop = buttonOffset.top + buttonHeight + tipHeight
+    let listHeight = this.listRef.offsetHeight
+
+    if (listTop + listHeight > window.innerHeight) {
+      listTop = window.innerHeight - listHeight - 10
+      this.tipRef.style.display = 'none'
+    } else {
+      this.tipRef.style.display = 'block'
+    }
+
+    this.listRef.style.top = `${listTop}px`
     this.listRef.style.left = `${buttonOffset.left}px`
   }
 
@@ -191,7 +201,7 @@ class Dropdown extends React.Component {
     let selectedLabel = this.getLabel(this.props.selectedItem)
     let list = ReactDOM.createPortal((
       <List {...this.props} innerRef={ref => { this.listRef = ref }}>
-        <Tip primary={this.state.firstItemHover} />
+        <Tip innerRef={ref => { this.tipRef = ref }} primary={this.state.firstItemHover} />
         <ListItems>
           {this.props.items.map((item, i) => {
             let label = this.getLabel(item)

+ 32 - 2
src/components/molecules/EndpointField/EndpointField.jsx

@@ -16,7 +16,13 @@ import React from 'react'
 import styled from 'styled-components'
 import PropTypes from 'prop-types'
 
-import { Switch, TextInput, Dropdown, RadioInput, InfoIcon } from 'components'
+import {
+  Switch,
+  TextInput,
+  Dropdown,
+  RadioInput,
+  InfoIcon,
+} from 'components'
 
 import LabelDictionary from '../../../utils/LabelDictionary'
 import StyleProps from '../../styleUtils/StyleProps'
@@ -48,13 +54,14 @@ class Field extends React.Component {
     large: PropTypes.bool,
     highlight: PropTypes.bool,
     disabled: PropTypes.bool,
+    enum: PropTypes.array,
   }
 
   renderSwitch() {
     return (
       <Switch
         disabled={this.props.disabled}
-        checked={this.props.value}
+        checked={this.props.value || false}
         onChange={checked => { this.props.onChange(checked) }}
       />
     )
@@ -75,6 +82,26 @@ class Field extends React.Component {
     )
   }
 
+  renderEnumDropdown() {
+    let items = this.props.enum.map(e => {
+      return {
+        label: LabelDictionary.get(e),
+        value: e,
+      }
+    })
+    let selectedItem = items.find(i => i.value === this.props.value)
+
+    return (
+      <Dropdown
+        large={this.props.large}
+        selectedItem={selectedItem}
+        items={items}
+        onChange={item => this.props.onChange(item.value)}
+        disabled={this.props.disabled}
+      />
+    )
+  }
+
   renderIntDropdown() {
     let items = []
 
@@ -112,6 +139,9 @@ class Field extends React.Component {
       case 'boolean':
         return this.renderSwitch()
       case 'string':
+        if (this.props.enum) {
+          return this.renderEnumDropdown()
+        }
         return this.renderTextInput()
       case 'integer':
         if (this.props.minimum || this.props.maximum) {

+ 15 - 7
src/components/molecules/Modal/Modal.jsx

@@ -69,8 +69,14 @@ class NewModal extends React.Component {
     window.removeEventListener('resize', this.positionModal, true)
   }
 
-  handleChildUpdate() {
-    setTimeout(this.positionModal, 100)
+  handleChildUpdate(scrollableRef, scrollOffset) {
+    if (scrollableRef) {
+      this.scrollableRef = scrollableRef
+    }
+
+    setTimeout(() => {
+      this.positionModal(scrollOffset)
+    }, 100)
   }
 
   handleModalOpen() {
@@ -89,14 +95,14 @@ class NewModal extends React.Component {
     window.scroll(0, this.windowScrollY)
   }
 
-  positionModal() {
+  positionModal(scrollOffset) {
     let pageNode = this.modalDiv && this.modalDiv.node.firstChild
     let contentNode = pageNode && pageNode.firstChild
     if (!contentNode) {
       return
     }
-
-    let scrollTop = contentNode.scrollTop
+    let scrollableNode = this.scrollableRef || contentNode
+    let scrollTop = scrollableNode.scrollTop
     contentNode.style.height = 'auto'
     let left = (pageNode.offsetWidth / 2) - (contentNode.offsetWidth / 2)
     let top = (pageNode.offsetHeight / 2) - (contentNode.offsetHeight / 2)
@@ -111,7 +117,7 @@ class NewModal extends React.Component {
     contentNode.style.top = `${top}px`
     contentNode.style.height = height
     contentNode.style.opacity = 1
-    contentNode.scrollTo(0, scrollTop)
+    scrollableNode.scrollTo(0, scrollTop + scrollOffset)
   }
 
   renderTitle() {
@@ -145,6 +151,8 @@ class NewModal extends React.Component {
         right: 'auto',
         transition: 'all 0.2s',
         opacity: 0,
+        display: 'flex',
+        flexDirection: 'column',
       },
     }
 
@@ -155,7 +163,7 @@ class NewModal extends React.Component {
 
     let children = React.Children.map(this.props.children,
       child => React.cloneElement(child, {
-        onResizeUpdate: () => { this.handleChildUpdate() },
+        onResizeUpdate: (scrollableRef, scrollOffset) => { this.handleChildUpdate(scrollableRef, scrollOffset) },
       })
     )
 

+ 66 - 138
src/components/organisms/Endpoint/Endpoint.jsx

@@ -17,7 +17,15 @@ import styled from 'styled-components'
 import PropTypes from 'prop-types'
 import connectToStores from 'alt-utils/lib/connectToStores'
 
-import { EndpointLogos, EndpointField, Button, StatusIcon, LoadingButton, CopyButton, Tooltip, StatusImage } from 'components'
+import {
+  EndpointLogos,
+  StatusIcon,
+  CopyButton,
+  Tooltip,
+  StatusImage,
+  Button,
+  LoadingButton,
+} from 'components'
 import NotificationActions from '../../../actions/NotificationActions'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointActions from '../../../actions/EndpointActions'
@@ -26,41 +34,24 @@ import ProviderActions from '../../../actions/ProviderActions'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import Palette from '../../styleUtils/Palette'
 import DomUtils from '../../../utils/DomUtils'
+import { ContentPlugin } from '../../../plugins/endpoint'
 
 const Wrapper = styled.div`
   padding: 48px 32px 32px 32px;
   display: flex;
   align-items: center;
   flex-direction: column;
-`
-const Fields = styled.div`
-  display: flex;
-  flex-wrap: wrap;
-  margin-left: -64px;
-  margin-top: 32px;
-`
-const FieldStyled = styled(EndpointField)`
-  margin-left: 64px;
-  min-width: 224px;
-  max-width: 224px;
-  margin-bottom: 16px;
-`
-const RadioGroup = styled.div`
-  width: 100%;
-`
-const Buttons = styled.div`
-  display: flex;
-  justify-content: space-between;
-  width: 100%;
-  margin-top: 32px;
+  min-height: 0;
 `
 const Status = styled.div`
   display: flex;
   flex-direction: column;
   align-items: center;
+  flex-shrink: 0;
 `
 const StatusHeader = styled.div`
   display: flex;
+  align-items: center;
 `
 const StatusMessage = styled.div`
   margin-left: 8px;
@@ -86,7 +77,12 @@ const StatusError = styled.div`
     margin-left: 4px;
   }
 `
-const Content = styled.div``
+const Content = styled.div`
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+`
 const LoadingWrapper = styled.div`
   display: flex;
   flex-direction: column;
@@ -97,6 +93,13 @@ const LoadingText = styled.div`
   font-size: 18px;
   margin-top: 32px;
 `
+const Buttons = styled.div`
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+  margin-top: 32px;
+  flex-shrink: 0;
+`
 
 class Endpoint extends React.Component {
   static propTypes = {
@@ -105,10 +108,8 @@ class Endpoint extends React.Component {
     deleteOnCancel: PropTypes.bool,
     endpoint: PropTypes.object,
     connectionInfo: PropTypes.object,
-    onFieldChange: PropTypes.func,
     onCancelClick: PropTypes.func,
     onResizeUpdate: PropTypes.func,
-    onValidateClick: PropTypes.func,
     endpointStore: PropTypes.object,
     providerStore: PropTypes.object,
   }
@@ -132,7 +133,6 @@ class Endpoint extends React.Component {
     super()
 
     this.state = {
-      fields: null,
       invalidFields: [],
       validating: false,
       showErrorMessage: false,
@@ -146,9 +146,6 @@ class Endpoint extends React.Component {
   }
 
   componentWillReceiveProps(props) {
-    let selectedRadio = this.getSelectedRadio(props.endpointStore.connectionInfo,
-      props.providerStore.connectionInfoSchema)
-
     if (this.state.validating) {
       if (props.endpointStore.validation && !props.endpointStore.validation.valid) {
         this.setState({ validating: false })
@@ -159,7 +156,6 @@ class Endpoint extends React.Component {
       this.setState({
         endpoint: {
           ...ObjectUtils.flatten(props.endpoint),
-          ...selectedRadio,
           ...ObjectUtils.flatten(props.endpointStore.connectionInfo),
         },
       })
@@ -168,17 +164,17 @@ class Endpoint extends React.Component {
         isNew: this.state.isNew === null || this.state.isNew,
         endpoint: {
           type: props.type,
-          ...selectedRadio,
           ...ObjectUtils.flatten(this.state.endpoint),
         },
       })
     }
 
-    this.props.onResizeUpdate()
+    this.props.onResizeUpdate(this.scrollableRef)
   }
 
   componentWillUnmount() {
     EndpointActions.clearValidation()
+    ProviderActions.clearConnectionInfoSchema()
     clearTimeout(this.closeTimeout)
   }
 
@@ -190,34 +186,7 @@ class Endpoint extends React.Component {
     return this.props.type
   }
 
-  getSelectedRadio(connectionInfo, schema) {
-    let radioGroup = schema.find(f => f.type === 'radio-group')
-
-    if (!radioGroup) {
-      return null
-    }
-
-    let selectedGroupItem = {}
-
-    if (!connectionInfo) {
-      selectedGroupItem[radioGroup.name] = radioGroup.default
-    } else {
-      radioGroup.items.forEach(i => {
-        let key = Object.keys(connectionInfo).find(k => k === i.name)
-        if (key) {
-          selectedGroupItem[radioGroup.name] = key
-        }
-      })
-    }
-
-    return selectedGroupItem
-  }
-
-  getFieldValue(field, parentGroup) {
-    if (parentGroup) {
-      return this.state.endpoint[parentGroup.name] === field.name
-    }
-
+  getFieldValue(field) {
     if (this.state.endpoint[field.name]) {
       return this.state.endpoint[field.name]
     }
@@ -233,23 +202,8 @@ class Endpoint extends React.Component {
     return this.state.validating
   }
 
-  findInvalidFields(invalidFields, schemaRoot) {
-    schemaRoot.forEach(field => {
-      if (field.type === 'radio-group') {
-        let selectedItem = field.items.find(i => i.name === this.state.endpoint[field.name])
-        this.findInvalidFields(invalidFields, selectedItem.fields)
-      } else if (field.required) {
-        let value = this.getFieldValue(field)
-        if (!value) {
-          invalidFields.push(field.name)
-        }
-      }
-    })
-  }
-
   highlightRequired() {
-    let invalidFields = []
-    this.findInvalidFields(invalidFields, this.props.providerStore.connectionInfoSchema)
+    let invalidFields = this.contentPluginRef.findInvalidFields()
     this.setState({ invalidFields })
     return invalidFields.length > 0
   }
@@ -270,20 +224,18 @@ class Endpoint extends React.Component {
     })
   }
 
-  handleFieldChange(field, value, parentGroup) {
+  handleFieldsChange(items) {
     let endpoint = { ...this.state.endpoint }
 
-    if (parentGroup) {
-      endpoint[parentGroup.name] = field.name
-    } else {
-      endpoint[field.name] = value
-    }
+    items.forEach(item => {
+      endpoint[item.field.name] = item.value
+    })
 
     this.setState({ endpoint })
   }
 
   handleValidateClick() {
-    if (!this.highlightRequired()) {
+    if (!this.highlightRequired(true)) {
       this.setState({ validating: true })
 
       NotificationActions.notify('Saving endpoint ...')
@@ -318,44 +270,6 @@ class Endpoint extends React.Component {
     this.props.onCancelClick()
   }
 
-  renderFields(fields, parentGroup) {
-    let renderedFields = []
-
-    fields.forEach(field => {
-      if (field.type === 'radio-group') {
-        renderedFields = renderedFields.concat(
-          <RadioGroup key={field.name}>{this.renderFields(field.items, field)}</RadioGroup>
-        )
-
-        field.items.forEach(item => {
-          if (this.getFieldValue(item, field)) {
-            renderedFields = renderedFields.concat(this.renderFields(item.fields))
-          }
-        })
-
-        return
-      }
-
-      renderedFields = renderedFields.concat(
-        <FieldStyled
-          {...field}
-          large
-          disabled={this.isValidating()
-            || (this.props.endpointStore.validation && this.props.endpointStore.validation.valid)}
-          key={field.name}
-          password={field.name === 'password'}
-          type={field.type}
-          highlight={this.state.invalidFields.findIndex(fn => fn === field.name) > -1}
-          name={field.name}
-          value={this.getFieldValue(field, parentGroup)}
-          onChange={value => { this.handleFieldChange(field, value, parentGroup) }}
-        />
-      )
-    })
-
-    return renderedFields
-  }
-
   renderEndpointStatus() {
     let validation = this.props.endpointStore.validation
     if (!this.isValidating() && !validation) {
@@ -396,21 +310,24 @@ class Endpoint extends React.Component {
     )
   }
 
-  renderActionButton() {
-    let button = <Button large onClick={() => this.handleValidateClick()}>Validate and save</Button>
+  renderButtons() {
+    let actionButton = <Button large onClick={() => this.handleValidateClick()}>Validate and save</Button>
 
     let message = 'Validating Endpoint ...'
-    let validation = this.props.endpointStore.validation
-
-    if (this.isValidating() || (validation && validation.valid)) {
-      if (validation && validation.valid) {
+    if (this.state.validating || (this.props.endpointStore.validation && this.props.endpointStore.validation.valid)) {
+      if (this.props.endpointStore.validation && this.props.endpointStore.validation.valid) {
         message = 'Saving ...'
       }
 
-      button = <LoadingButton large>{message}</LoadingButton>
+      actionButton = <LoadingButton large>{message}</LoadingButton>
     }
 
-    return button
+    return (
+      <Buttons>
+        <Button large secondary onClick={() => { this.handleCancelClick() }}>{this.props.cancelButtonText}</Button>
+        {actionButton}
+      </Buttons>
+    )
   }
 
   renderContent() {
@@ -421,15 +338,26 @@ class Endpoint extends React.Component {
     return (
       <Content>
         {this.renderEndpointStatus()}
-        <Fields>
-          {this.renderFields(this.props.providerStore.connectionInfoSchema)}
-          <Tooltip />
-          {Tooltip.rebuild()}
-        </Fields>
-        <Buttons>
-          <Button large secondary onClick={() => { this.handleCancelClick() }}>{this.props.cancelButtonText}</Button>
-          {this.renderActionButton()}
-        </Buttons>
+        {React.createElement(ContentPlugin[this.getEndpointType()] || ContentPlugin.default, {
+          connectionInfoSchema: this.props.providerStore.connectionInfoSchema,
+          validation: this.props.endpointStore.validation,
+          invalidFields: this.state.invalidFields,
+          validating: this.state.validating,
+          disabled: this.isValidating() || (this.props.endpointStore.validation && this.props.endpointStore.validation.valid),
+          cancelButtonText: this.props.cancelButtonText,
+          getFieldValue: field => this.getFieldValue(field),
+          highlightRequired: () => this.highlightRequired(),
+          handleFieldChange: (field, value) => { this.handleFieldsChange([{ field, value }]) },
+          handleFieldsChange: fields => { this.handleFieldsChange(fields) },
+          handleValidateClick: () => { this.handleValidateClick() },
+          handleCancelClick: () => { this.handleCancelClick() },
+          scrollableRef: ref => { this.scrollableRef = ref },
+          onRef: ref => { this.contentPluginRef = ref },
+          onResizeUpdate: (scrollableRef, scrollOffset) => { this.props.onResizeUpdate(this.scrollableRef, scrollOffset) },
+        })}
+        {this.renderButtons()}
+        <Tooltip />
+        {Tooltip.rebuild()}
       </Content>
     )
   }

+ 14 - 3
src/components/organisms/PageHeader/PageHeader.jsx

@@ -63,6 +63,8 @@ class PageHeader extends React.Component {
   static propTypes = {
     title: PropTypes.string.isRequired,
     onProjectChange: PropTypes.func,
+    onModalOpen: PropTypes.func,
+    onModalClose: PropTypes.func,
     projectStore: PropTypes.object,
     userStore: PropTypes.object,
     providerStore: PropTypes.object,
@@ -119,6 +121,9 @@ class PageHeader extends React.Component {
     switch (item.value) {
       case 'endpoint':
         ProviderActions.loadProviders()
+        if (this.props.onModalOpen) {
+          this.props.onModalOpen()
+        }
         this.setState({ showChooseProviderModal: true })
         break
       default:
@@ -130,6 +135,9 @@ class PageHeader extends React.Component {
   }
 
   handleCloseChooseProviderModal() {
+    if (this.props.onModalClose) {
+      this.props.onModalClose()
+    }
     this.setState({ showChooseProviderModal: false })
   }
 
@@ -142,11 +150,14 @@ class PageHeader extends React.Component {
   }
 
   handleCloseEndpointModal() {
+    if (this.props.onModalClose) {
+      this.props.onModalClose()
+    }
     this.setState({ showEndpointModal: false })
   }
 
-  handleBackEndpointModal() {
-    this.setState({ showChooseProviderModal: true, showEndpointModal: false })
+  handleBackEndpointModal(options) {
+    this.setState({ showChooseProviderModal: !options || !options.autoClose, showEndpointModal: false })
   }
 
   render() {
@@ -186,7 +197,7 @@ class PageHeader extends React.Component {
             deleteOnCancel
             type={this.state.providerType}
             cancelButtonText="Back"
-            onCancelClick={() => { this.handleBackEndpointModal() }}
+            onCancelClick={options => { this.handleBackEndpointModal(options) }}
           />
         </Modal>
       </Wrapper>

+ 16 - 0
src/components/pages/ReplicasPage/ReplicasPage.jsx

@@ -68,6 +68,7 @@ class ReplicasPage extends React.Component {
     this.state = {
       showDeleteReplicaConfirmation: false,
       confirmationItems: null,
+      modalIsOpen: false,
     }
   }
 
@@ -165,7 +166,20 @@ class ReplicasPage extends React.Component {
     window.location.href = '/#/wizard/replica'
   }
 
+  handleModalOpen() {
+    this.setState({ modalIsOpen: true })
+  }
+
+  handleModalClose() {
+    this.setState({ modalIsOpen: false }, () => {
+      this.pollData()
+    })
+  }
+
   pollData() {
+    if (this.state.modalIsOpen) {
+      return
+    }
     ReplicaActions.getReplicas().promise.then(() => {
       this.pollTimeout = setTimeout(() => { this.pollData() }, requestPollTimeout)
     })
@@ -232,6 +246,8 @@ class ReplicasPage extends React.Component {
             <PageHeader
               title="Coriolis Replicas"
               onProjectChange={project => { this.handleProjectChange(project) }}
+              onModalOpen={() => { this.handleModalOpen() }}
+              onModalClose={() => { this.handleModalClose() }}
             />
           }
         />

+ 305 - 0
src/plugins/endpoint/azure/ContentPlugin.jsx

@@ -0,0 +1,305 @@
+/*
+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 { TextArea } from 'components'
+import Palette from '../../../components/styleUtils/Palette'
+import StyleProps from '../../../components/styleUtils/StyleProps'
+import { Wrapper, Fields, FieldStyled, Row } from '../default/ContentPlugin'
+
+const RadioGroup = styled.div`
+  width: 100%;
+`
+const PasteWrapper = styled.div``
+const PasteLabel = styled.div`
+  display: flex;
+  font-size: 10px;
+`
+const PasteLabelText = styled.div`
+  font-weight: ${StyleProps.fontWeights.medium};
+  color: ${Palette.grayscale[3]};
+  text-transform: uppercase;
+  margin-bottom: 4px;
+`
+const PasteLabelShowMore = styled.div`
+  color: ${Palette.primary};
+  margin-left: 5px;
+  cursor: pointer;
+`
+
+const fieldNameMapper = {
+  activeDirectory: 'active_directory_url',
+  activeDirectoryDataLakeResourceId: 'active_directory_data_lake_resource_id',
+  activeDirectoryGraphResourceId: 'active_directory_graph_resource_id',
+  activeDirectoryResourceId: 'active_directory_resource_id',
+  batchResourceId: 'batch_resource_endpoint',
+  gallery: 'gallery_endpoint',
+  management: 'management_endpoint',
+  resourceManager: 'resource_manager_endpoint',
+  sqlManagement: 'sql_management_endpoint',
+  vmImageAliasDoc: 'vm_image_alias_doc',
+  azureDatalakeAnalyticsCatalogAndJobEndpoint: 'azure_datalake_analytics_catalog_and_job_endpoint',
+  azureDatalakeStoreFileSystemEndpoint: 'azure_datalake_store_file_system_endpoint',
+  keyvaultDns: 'keyvault_dns',
+  sqlServerHostname: 'sql_server_hostname',
+  storageEndpoint: 'storage_endpoint',
+}
+
+class ContentPlugin extends React.Component {
+  static propTypes = {
+    connectionInfoSchema: PropTypes.array,
+    validation: PropTypes.object,
+    invalidFields: PropTypes.array,
+    getFieldValue: PropTypes.func,
+    handleFieldChange: PropTypes.func,
+    handleFieldsChange: PropTypes.func,
+    disabled: PropTypes.bool,
+    cancelButtonText: PropTypes.string,
+    validating: PropTypes.bool,
+    handleValidateClick: PropTypes.func,
+    handleCancelClick: PropTypes.func,
+    highlightRequired: PropTypes.func,
+    onRef: PropTypes.func,
+    onResizeUpdate: PropTypes.func,
+    scrollableRef: PropTypes.func,
+  }
+
+  constructor() {
+    super()
+
+    this.state = {
+      jsonConfig: '',
+      showPasteInput: false,
+    }
+  }
+
+  componentDidMount() {
+    this.props.onRef(this)
+  }
+
+  componentDidUpdate(prevProps, prevState) {
+    if (this.cloudProfileChanged || prevState.showPasteInput !== this.state.showPasteInput) {
+      let scrollOffset = 0
+      if (prevState.showPasteInput !== this.state.showPasteInput && this.state.showPasteInput) {
+        scrollOffset = 100
+      }
+      this.props.onResizeUpdate(this.fieldsRef, scrollOffset)
+      this.cloudProfileChanged = false
+    }
+  }
+
+  componentWillUnmount() {
+    this.props.onRef(undefined)
+  }
+
+  findInvalidFields = () => {
+    let invalidFields = []
+    const find = fields => {
+      fields.forEach(field => {
+        if (field.required) {
+          let value = this.props.getFieldValue(field)
+          if (!value) {
+            invalidFields.push(field.name)
+          }
+        }
+      })
+    }
+    find(this.props.connectionInfoSchema)
+
+    let loginTypeField = this.props.connectionInfoSchema.find(f => f.name === 'login_type')
+    let selectedLoginTypeField = loginTypeField.items.find(f => f.name === this.props.getFieldValue(loginTypeField))
+    find(selectedLoginTypeField.fields)
+
+    if (this.props.getFieldValue({ name: 'cloud_profile' }) === 'CustomCloud') {
+      let customCloudFields = this.props.connectionInfoSchema.find(f => f.name === 'cloud_profile').custom_cloud_fields
+      find(customCloudFields)
+    }
+
+    return invalidFields
+  }
+
+  handleJsonConfigBlur() {
+    if (this.lastBlurValue && this.lastBlurValue === this.state.jsonConfig) {
+      return
+    }
+    this.lastBlurValue = this.state.jsonConfig
+
+    let json
+    try {
+      json = JSON.parse(this.state.jsonConfig)
+    } catch (e) {
+      return
+    }
+
+    if (!json.endpoints || !json.suffixes) {
+      return
+    }
+
+    let managementUrl = json.endpoints.management
+    if (managementUrl && !json.endpoints.sqlManagement) {
+      if (managementUrl.lastIndexOf('/') === managementUrl.length - 1) {
+        managementUrl = managementUrl.substr(0, managementUrl.length - 1)
+      }
+      json.endpoints.sqlManagement = `${managementUrl}:8443/`
+    }
+
+    let updatedFields = []
+    const setValue = (object, key) => {
+      if (object[key]) {
+        updatedFields.push({ field: { name: fieldNameMapper[key] }, value: object[key] })
+      }
+    }
+    Object.keys(json.endpoints).forEach(k => {
+      setValue(json.endpoints, k)
+    })
+    Object.keys(json.suffixes).forEach(k => {
+      setValue(json.suffixes, k)
+    })
+    this.props.handleFieldsChange(updatedFields)
+  }
+
+  handleFieldChange(field, value) {
+    if (field.name === 'cloud_profile') {
+      this.cloudProfileChanged = true
+    }
+    this.props.handleFieldChange(field, value)
+  }
+
+  renderField(field, customProps) {
+    return (
+      <FieldStyled
+        {...field}
+        large
+        disabled={this.props.disabled}
+        key={field.name}
+        password={field.name === 'password'}
+        highlight={this.props.invalidFields.findIndex(fn => fn === field.name) > -1}
+        value={this.props.getFieldValue(field)}
+        onChange={value => { this.handleFieldChange(field, value) }}
+        {...customProps}
+      />
+    )
+  }
+
+  renderFieldRows(fields) {
+    const rows = []
+    let lastField
+    fields.forEach((field, i) => {
+      const currentField = this.renderField(field)
+      if (i % 2 !== 0) {
+        rows.push((
+          <Row key={field.name}>
+            {lastField}
+            {currentField}
+          </Row>
+        ))
+      } else if (i === fields.length - 1) {
+        rows.push((
+          <Row key={field.name}>
+            {currentField}
+          </Row>
+        ))
+      }
+      lastField = currentField
+    })
+
+    return rows
+  }
+
+  renderPasteField() {
+    const textArea = (
+      <TextArea
+        width="100%"
+        height="96px"
+        placeholder="Use the Azure CLI to get the details of a registered cloud and paste it here"
+        value={this.state.jsonConfig}
+        onBlur={() => { this.handleJsonConfigBlur() }}
+        onChange={e => { this.setState({ jsonConfig: e.target.value }) }}
+        disabled={this.props.disabled}
+      />
+    )
+
+    return (
+      <PasteWrapper>
+        <PasteLabel>
+          <PasteLabelText>Paste Configuration (optional)</PasteLabelText>
+          <PasteLabelShowMore
+            onClick={() => { this.setState({ showPasteInput: !this.state.showPasteInput }) }}
+          >{this.state.showPasteInput ? 'Hide' : 'Show'}</PasteLabelShowMore>
+        </PasteLabel>
+        {this.state.showPasteInput ? textArea : null}
+      </PasteWrapper>
+    )
+  }
+
+  renderFields() {
+    const fields = this.props.connectionInfoSchema
+    const cloudProfileField = fields.find(f => f.name === 'cloud_profile')
+    const loginTypeField = fields.find(f => f.name === 'login_type')
+    const allowUntrustedField = loginTypeField.items.find(f => f.name === this.props.getFieldValue(loginTypeField)).fields.find(f => f.name === 'allow_untrusted')
+
+    let renderedFields = this.renderFieldRows(fields.filter(f => f.name !== loginTypeField.name && f.name !== cloudProfileField.name))
+
+    const radioGroupRow = (
+      <Row key="radio-group-row">
+        <RadioGroup key="radio-group">
+          {loginTypeField.items.map(field =>
+            this.renderField(field, {
+              value: this.props.getFieldValue(loginTypeField) === field.name,
+              onChange: value => { if (value) this.props.handleFieldChange(loginTypeField, field.name) },
+            })
+          )}
+        </RadioGroup>
+        {this.renderField(allowUntrustedField)}
+      </Row>
+    )
+
+    renderedFields.push(radioGroupRow)
+    renderedFields = renderedFields.concat(this.renderFieldRows(
+      loginTypeField.items.find(f => f.name === this.props.getFieldValue(loginTypeField)).fields
+        .filter(f => f.name !== allowUntrustedField.name)
+        .concat([cloudProfileField])
+    ))
+
+    const isCustomCloud = this.props.getFieldValue(cloudProfileField) === 'CustomCloud'
+    if (isCustomCloud) {
+      renderedFields = renderedFields.concat(this.renderFieldRows(cloudProfileField.custom_cloud_fields))
+    }
+
+    return (
+      <Fields innerRef={ref => { this.props.scrollableRef(ref) }}>
+        {renderedFields}
+        {isCustomCloud ? this.renderPasteField() : null}
+      </Fields>
+    )
+  }
+
+  render() {
+    const fields = this.props.connectionInfoSchema
+    if (fields.length === 0) {
+      return null
+    }
+
+    return (
+      <Wrapper>
+        {this.renderFields()}
+      </Wrapper>
+    )
+  }
+}
+
+export default ContentPlugin

+ 115 - 0
src/plugins/endpoint/azure/SchemaPlugin.js

@@ -0,0 +1,115 @@
+/*
+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 {
+  connectionSchemaToFields,
+  defaultSchemaToFields,
+  generateField,
+  fieldsToPayload,
+} from '../default/SchemaPlugin'
+
+const fieldsToPayloadUseDefaults = (data, schema) => {
+  let info = {}
+
+  Object.keys(schema.properties).forEach(fieldName => {
+    if (typeof data[fieldName] === 'object') {
+      return
+    }
+    if (data[fieldName]) {
+      info[fieldName] = data[fieldName]
+    } else if (schema.properties[fieldName].default) {
+      info[fieldName] = schema.properties[fieldName].default
+    }
+  })
+
+  return info
+}
+
+const azureConnectionParse = schema => {
+  let commonFields = connectionSchemaToFields(schema).filter(f => f.type !== 'object' && f.name !== 'secret_ref' && Object.keys(f).findIndex(k => k === 'enum') === -1)
+
+  let getOption = name => {
+    return {
+      name,
+      type: 'radio',
+      fields: [
+        ...connectionSchemaToFields(schema.properties[name]),
+        ...commonFields,
+      ],
+    }
+  }
+
+  let radioGroup = {
+    name: 'login_type',
+    default: 'user_credentials',
+    type: 'radio-group',
+    items: [getOption('user_credentials'), getOption('service_principal_credentials')],
+  }
+
+  let cloudProfileDropdown = {
+    name: 'cloud_profile',
+    type: 'string',
+    ...schema.properties.cloud_profile,
+    custom_cloud_fields: [
+      ...defaultSchemaToFields(schema.properties.custom_cloud_properties.properties.endpoints),
+      ...defaultSchemaToFields(schema.properties.custom_cloud_properties.properties.suffixes),
+    ],
+  }
+
+  return [radioGroup, cloudProfileDropdown]
+}
+
+export default class ConnectionSchemaParser {
+  static parseSchemaToFields(schema) {
+    let fields = azureConnectionParse(schema)
+
+    fields = [
+      generateField('name', 'Endpoint Name', true),
+      generateField('description', 'Endpoint Description'),
+      ...fields,
+    ]
+
+    return fields
+  }
+
+  static parseFieldsToPayload(data, schema) {
+    let payload = {}
+
+    payload.name = data.name
+    payload.description = data.description
+
+    let connectionInfo = fieldsToPayload(data, schema)
+    let loginType = data.login_type || 'user_credentials'
+    connectionInfo[loginType] = fieldsToPayload(data, schema.properties[loginType])
+
+    if (data.cloud_profile === 'CustomCloud') {
+      connectionInfo.custom_cloud_properties = {
+        endpoints: {
+          ...fieldsToPayloadUseDefaults(data, schema.properties.custom_cloud_properties.properties.endpoints),
+        },
+        suffixes: {
+          ...fieldsToPayloadUseDefaults(data, schema.properties.custom_cloud_properties.properties.suffixes),
+        },
+      }
+    }
+
+    if (data.secret_ref) {
+      connectionInfo.secret_ref = data.secret_ref
+    }
+
+    payload.connection_info = connectionInfo
+
+    return payload
+  }
+}

+ 126 - 0
src/plugins/endpoint/default/ContentPlugin.jsx

@@ -0,0 +1,126 @@
+/*
+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 { EndpointField } from '../../../components'
+
+export const Wrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+`
+export const Fields = styled.div`
+  display: flex;
+  margin-top: 32px;
+  flex-direction: column;
+  overflow: auto;
+`
+export const FieldStyled = styled(EndpointField)`
+  min-width: 224px;
+  max-width: 224px;
+  margin-bottom: 16px;
+`
+export const Row = styled.div`
+  display: flex;
+  flex-shrink: 0;
+  justify-content: space-between;
+`
+
+class ContentPlugin extends React.Component {
+  static propTypes = {
+    connectionInfoSchema: PropTypes.array,
+    validation: PropTypes.object,
+    invalidFields: PropTypes.array,
+    getFieldValue: PropTypes.func,
+    handleFieldChange: PropTypes.func,
+    disabled: PropTypes.bool,
+    cancelButtonText: PropTypes.string,
+    validating: PropTypes.bool,
+    handleValidateClick: PropTypes.func,
+    handleCancelClick: PropTypes.func,
+    onRef: PropTypes.func,
+  }
+
+  componentDidMount() {
+    this.props.onRef(this)
+  }
+
+  componentWillUnmount() {
+    this.props.onRef(undefined)
+  }
+
+  findInvalidFields = () => {
+    const invalidFields = this.props.connectionInfoSchema.filter(field => {
+      if (field.required) {
+        let value = this.props.getFieldValue(field)
+        return !value
+      }
+      return false
+    }).map(f => f.name)
+
+    return invalidFields
+  }
+
+  renderFields() {
+    const rows = []
+    let lastField
+    this.props.connectionInfoSchema.forEach((field, i) => {
+      const currentField = (
+        <FieldStyled
+          {...field}
+          large
+          disabled={this.props.disabled}
+          password={field.name === 'password'}
+          highlight={this.props.invalidFields.findIndex(fn => fn === field.name) > -1}
+          value={this.props.getFieldValue(field)}
+          onChange={value => { this.props.handleFieldChange(field, value) }}
+        />
+      )
+      if (i % 2 !== 0) {
+        rows.push((
+          <Row key={field.name}>
+            {lastField}
+            {currentField}
+          </Row>
+        ))
+      } else if (i === this.props.connectionInfoSchema.length - 1) {
+        rows.push((
+          <Row key={field.name}>
+            {currentField}
+          </Row>
+        ))
+      }
+      lastField = currentField
+    })
+
+    return (
+      <Fields>
+        {rows}
+      </Fields>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {this.renderFields()}
+      </Wrapper>
+    )
+  }
+}
+
+export default ContentPlugin

+ 102 - 0
src/plugins/endpoint/default/SchemaPlugin.js

@@ -0,0 +1,102 @@
+/*
+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/>.
+*/
+
+export const defaultSchemaToFields = schema => {
+  let fields = Object.keys(schema.properties).map(fieldName => {
+    let field = {
+      ...schema.properties[fieldName],
+      name: fieldName,
+      required: schema.required && schema.required.find(k => k === fieldName) ? true : fieldName === 'username' || fieldName === 'password',
+    }
+    return field
+  })
+
+  return fields
+}
+
+export const connectionSchemaToFields = schema => {
+  let fields = defaultSchemaToFields(schema)
+
+  let sortPriority = { username: 1, password: 2 }
+  fields.sort((a, b) => {
+    if (sortPriority[a.name] && sortPriority[b.name]) {
+      return sortPriority[a.name] - sortPriority[b.name]
+    }
+    if (sortPriority[a.name] || (a.required && !b.required)) {
+      return -1
+    }
+    if (sortPriority[b.name] || (!a.required && b.required)) {
+      return 1
+    }
+    return a.name.localeCompare(b.name)
+  })
+
+  return fields
+}
+
+export const generateField = (name, label, required = false, type = 'string', defaultValue = null) => {
+  let field = {
+    name,
+    label,
+    type,
+    required,
+  }
+
+  if (defaultValue) {
+    field.default = defaultValue
+  }
+
+  return field
+}
+
+export const fieldsToPayload = (data, schema) => {
+  let info = {}
+
+  Object.keys(schema.properties).forEach(fieldName => {
+    if (data[fieldName] && typeof data[fieldName] !== 'object') {
+      info[fieldName] = data[fieldName]
+    }
+  })
+
+  return info
+}
+
+export default class ConnectionSchemaParser {
+  static parseSchemaToFields(schema) {
+    let fields = connectionSchemaToFields(schema.oneOf[0])
+
+    fields = [
+      generateField('name', 'Endpoint Name', true),
+      generateField('description', 'Endpoint Description'),
+      ...fields,
+    ]
+
+    return fields
+  }
+
+  static parseFieldsToPayload(data, schema) {
+    let payload = {}
+
+    payload.name = data.name
+    payload.description = data.description
+
+    payload.connection_info = fieldsToPayload(data, schema.oneOf[0])
+
+    if (data.secret_ref) {
+      payload.connection_info.secret_ref = data.secret_ref
+    }
+
+    return payload
+  }
+}

+ 28 - 0
src/plugins/endpoint/index.js

@@ -0,0 +1,28 @@
+/*
+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 DefaultSchemaPlugin from './default/SchemaPlugin'
+import AzureSchemaPlugin from './azure/SchemaPlugin'
+import DefaultContentPlugin from './default/ContentPlugin'
+import AzureContentPlugin from './azure/ContentPlugin'
+
+export const SchemaPlugin = {
+  default: DefaultSchemaPlugin,
+  azure: AzureSchemaPlugin,
+}
+
+export const ContentPlugin = {
+  default: DefaultContentPlugin,
+  azure: AzureContentPlugin,
+}

+ 7 - 125
src/sources/Schemas.js

@@ -12,131 +12,25 @@ 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/>.
 */
 
-let parseToFields = schema => {
-  let fields = Object.keys(schema.properties).map(fieldName => {
-    let field = {
-      ...schema.properties[fieldName],
-      name: fieldName,
-      required: schema.required && schema.required.find(k => k === fieldName) ? true : fieldName === 'username' || fieldName === 'password',
-    }
-    return field
-  })
-
-  return fields
-}
-
-let connectionParseToFields = schema => {
-  let fields = parseToFields(schema)
-
-  let sortPriority = { username: 1, password: 2 }
-  fields.sort((a, b) => {
-    if (sortPriority[a.name] && sortPriority[b.name]) {
-      return sortPriority[a.name] - sortPriority[b.name]
-    }
-    if (sortPriority[a.name] || (a.required && !b.required)) {
-      return -1
-    }
-    if (sortPriority[b.name] || (!a.required && b.required)) {
-      return 1
-    }
-    return a.name.localeCompare(b.name)
-  })
-
-  return fields
-}
-
-let connectionParsersToFields = {
-  general: schema => {
-    return connectionParseToFields(schema.oneOf[0])
-  },
-  azure: schema => {
-    let commonFields = connectionParseToFields(schema).filter(f => f.type !== 'object' && f.name !== 'secret_ref')
-
-    let getOption = (option) => {
-      return {
-        name: option,
-        type: 'radio',
-        fields: [
-          ...connectionParseToFields(schema.properties[option]),
-          ...commonFields,
-        ],
-      }
-    }
-
-    let radioGroup = {
-      name: 'login_type',
-      default: 'user_credentials',
-      type: 'radio-group',
-      items: [getOption('user_credentials'), getOption('service_principal_credentials')],
-    }
-
-    return [radioGroup]
-  },
-}
-
-let parseToPayload = (data, schema) => {
-  let info = {}
-
-  Object.keys(schema.properties).forEach(fieldName => {
-    if (data[fieldName] && typeof data[fieldName] !== 'object') {
-      info[fieldName] = data[fieldName]
-    }
-  })
-
-  return info
-}
-
-let parsersToPayload = {
-  general: (data, schema) => {
-    return parseToPayload(data, schema.oneOf[0])
-  },
-  azure: (data, schema) => {
-    let payload = parseToPayload(data, schema)
-    payload[data.login_type] = parseToPayload(data, schema.properties[data.login_type])
-    return payload
-  },
-}
+import { SchemaPlugin } from '../plugins/endpoint'
+import { defaultSchemaToFields } from '../plugins/endpoint/default/SchemaPlugin'
 
 class SchemaParser {
   static storedConnectionsSchemas = {}
 
-  static generateField(name, label, required = false, type = 'string', defaultValue = null) {
-    let field = {
-      name,
-      label,
-      type,
-      required,
-    }
-
-    if (defaultValue) {
-      field.default = defaultValue
-    }
-
-    return field
-  }
-
   static connectionSchemaToFields(provider, schema) {
     if (!this.storedConnectionsSchemas[provider]) {
       this.storedConnectionsSchemas[provider] = schema
     }
 
-    if (!connectionParsersToFields[provider]) {
-      provider = 'general'
-    }
-
-    let fields = connectionParsersToFields[provider](schema)
-
-    fields = [
-      this.generateField('name', 'Endpoint Name', true),
-      this.generateField('description', 'Endpoint Description'),
-      ...fields,
-    ]
+    let parsers = SchemaPlugin[provider] || SchemaPlugin.default
+    let fields = parsers.parseSchemaToFields(schema)
 
     return fields
   }
 
   static optionsSchemaToFields(provider, schema) {
-    let fields = parseToFields(schema.oneOf[0])
+    let fields = defaultSchemaToFields(schema.oneOf[0])
     fields.sort((a, b) => {
       if (a.required && !b.required) {
         return -1
@@ -153,20 +47,8 @@ class SchemaParser {
 
   static fieldsToPayload(data) {
     let storedSchema = this.storedConnectionsSchemas[data.type] || this.storedConnectionsSchemas.general
-    let payload = {}
-
-    payload.name = data.name
-    payload.description = data.description
-
-    if (parsersToPayload[data.type]) {
-      payload.connection_info = parsersToPayload[data.type](data, storedSchema)
-    } else {
-      payload.connection_info = parsersToPayload.general(data, storedSchema)
-    }
-
-    if (data.secret_ref) {
-      payload.connection_info.secret_ref = data.secret_ref
-    }
+    let parsers = SchemaPlugin[data.type] || SchemaPlugin.default
+    let payload = parsers.parseFieldsToPayload(data, storedSchema)
 
     return payload
   }

+ 5 - 0
src/stores/ProviderStore.js

@@ -28,6 +28,7 @@ class ProviderStore {
       handleGetConnectionInfoSchema: ProviderActions.GET_CONNECTION_INFO_SCHEMA,
       handleGetConnectionInfoSchemaSuccess: ProviderActions.GET_CONNECTION_INFO_SCHEMA_SUCCESS,
       handleGetConnectionInfoSchemaFailed: ProviderActions.GET_CONNECTION_INFO_SCHEMA_FAILED,
+      handleClearConnectionInfoSchema: ProviderActions.CLEAR_CONNECTION_INFO_SCHEMA,
       handleLoadProviders: ProviderActions.LOAD_PROVIDERS,
       handleLoadProvidersSuccess: ProviderActions.LOAD_PROVIDERS_SUCCESS,
       handleLoadOptionsSchema: ProviderActions.LOAD_OPTIONS_SCHEMA,
@@ -49,6 +50,10 @@ class ProviderStore {
     this.connectionSchemaLoading = false
   }
 
+  handleClearConnectionInfoSchema() {
+    this.connectionInfoSchema = []
+  }
+
   handleLoadProviders() {
     this.providers = null
     this.providersLoading = true