2
0
Эх сурвалжийг харах

Implement Endpoint Content plugin

Sergiu Miclea 8 жил өмнө
parent
commit
e8a11744a6

+ 1 - 1
src/actions/EndpointActions.js

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

+ 4 - 0
src/actions/ProviderActions.js

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

+ 26 - 343
src/components/organisms/Endpoint/Endpoint.jsx

@@ -19,15 +19,10 @@ import connectToStores from 'alt-utils/lib/connectToStores'
 
 
 import {
 import {
   EndpointLogos,
   EndpointLogos,
-  EndpointField,
-  Button,
   StatusIcon,
   StatusIcon,
-  LoadingButton,
   CopyButton,
   CopyButton,
   Tooltip,
   Tooltip,
   StatusImage,
   StatusImage,
-  RadioInput,
-  TextArea,
 } from 'components'
 } from 'components'
 import NotificationActions from '../../../actions/NotificationActions'
 import NotificationActions from '../../../actions/NotificationActions'
 import EndpointStore from '../../../stores/EndpointStore'
 import EndpointStore from '../../../stores/EndpointStore'
@@ -37,7 +32,7 @@ import ProviderActions from '../../../actions/ProviderActions'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import DomUtils from '../../../utils/DomUtils'
 import DomUtils from '../../../utils/DomUtils'
-import StyleProps from '../../styleUtils/StyleProps'
+import { ContentPlugin } from '../../../plugins/endpoint'
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
   padding: 48px 32px 32px 32px;
   padding: 48px 32px 32px 32px;
@@ -45,27 +40,6 @@ const Wrapper = styled.div`
   align-items: center;
   align-items: center;
   flex-direction: column;
   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;
-`
 const Status = styled.div`
 const Status = styled.div`
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
@@ -111,31 +85,6 @@ const LoadingText = styled.div`
   font-size: 18px;
   font-size: 18px;
   margin-top: 32px;
   margin-top: 32px;
 `
 `
-const ConfigLabel = styled.div`
-  font-size: 11px;
-  margin-top: -10px;
-  color: ${Palette.grayscale[3]};
-`
-const AdditionalConfigTitle = styled.div`
-  text-align: center;
-`
-const AzureConfigInputType = styled.div`
-  margin-top: 32px;
-  > div {
-    margin-bottom: 16px;
-  }
-`
-const PasteField = styled.div`
-  margin-top: 32px;
-`
-const PasteFieldLabel = styled.div`
-  font-size: 10px;
-  font-weight: ${StyleProps.fontWeights.medium};
-  color: ${Palette.grayscale[3]};
-  text-transform: uppercase;
-  margin-bottom: 4px;
-`
-const PasteFieldInput = styled.div``
 
 
 class Endpoint extends React.Component {
 class Endpoint extends React.Component {
   static propTypes = {
   static propTypes = {
@@ -144,10 +93,8 @@ class Endpoint extends React.Component {
     deleteOnCancel: PropTypes.bool,
     deleteOnCancel: PropTypes.bool,
     endpoint: PropTypes.object,
     endpoint: PropTypes.object,
     connectionInfo: PropTypes.object,
     connectionInfo: PropTypes.object,
-    onFieldChange: PropTypes.func,
     onCancelClick: PropTypes.func,
     onCancelClick: PropTypes.func,
     onResizeUpdate: PropTypes.func,
     onResizeUpdate: PropTypes.func,
-    onValidateClick: PropTypes.func,
     endpointStore: PropTypes.object,
     endpointStore: PropTypes.object,
     providerStore: PropTypes.object,
     providerStore: PropTypes.object,
   }
   }
@@ -171,15 +118,11 @@ class Endpoint extends React.Component {
     super()
     super()
 
 
     this.state = {
     this.state = {
-      fields: null,
       invalidFields: [],
       invalidFields: [],
       validating: false,
       validating: false,
       showErrorMessage: false,
       showErrorMessage: false,
       endpoint: {},
       endpoint: {},
       isNew: null,
       isNew: null,
-      showAdditionalConfig: false, // if azure
-      additionConfigManualInput: false, // if azure
-      masJsonConfig: '', // if azure
     }
     }
   }
   }
 
 
@@ -188,9 +131,6 @@ class Endpoint extends React.Component {
   }
   }
 
 
   componentWillReceiveProps(props) {
   componentWillReceiveProps(props) {
-    let selectedRadio = this.getSelectedRadio(props.endpointStore.connectionInfo,
-      props.providerStore.connectionInfoSchema)
-
     if (this.state.validating) {
     if (this.state.validating) {
       if (props.endpointStore.validation && !props.endpointStore.validation.valid) {
       if (props.endpointStore.validation && !props.endpointStore.validation.valid) {
         this.setState({ validating: false })
         this.setState({ validating: false })
@@ -201,7 +141,6 @@ class Endpoint extends React.Component {
       this.setState({
       this.setState({
         endpoint: {
         endpoint: {
           ...ObjectUtils.flatten(props.endpoint),
           ...ObjectUtils.flatten(props.endpoint),
-          ...selectedRadio,
           ...ObjectUtils.flatten(props.endpointStore.connectionInfo),
           ...ObjectUtils.flatten(props.endpointStore.connectionInfo),
         },
         },
       })
       })
@@ -210,7 +149,6 @@ class Endpoint extends React.Component {
         isNew: this.state.isNew === null || this.state.isNew,
         isNew: this.state.isNew === null || this.state.isNew,
         endpoint: {
         endpoint: {
           type: props.type,
           type: props.type,
-          ...selectedRadio,
           ...ObjectUtils.flatten(this.state.endpoint),
           ...ObjectUtils.flatten(this.state.endpoint),
         },
         },
       })
       })
@@ -219,16 +157,9 @@ class Endpoint extends React.Component {
     this.props.onResizeUpdate()
     this.props.onResizeUpdate()
   }
   }
 
 
-  // if azure?
-  componentDidUpdate(prevProps, prevState) {
-    if (prevState.showAdditionalConfig !== this.state.showAdditionalConfig ||
-      prevState.additionConfigManualInput !== this.state.additionConfigManualInput) {
-      this.props.onResizeUpdate()
-    }
-  }
-
   componentWillUnmount() {
   componentWillUnmount() {
     EndpointActions.clearValidation()
     EndpointActions.clearValidation()
+    ProviderActions.clearConnectionInfoSchema()
     clearTimeout(this.closeTimeout)
     clearTimeout(this.closeTimeout)
   }
   }
 
 
@@ -240,34 +171,7 @@ class Endpoint extends React.Component {
     return this.props.type
     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]) {
     if (this.state.endpoint[field.name]) {
       return this.state.endpoint[field.name]
       return this.state.endpoint[field.name]
     }
     }
@@ -283,26 +187,8 @@ class Endpoint extends React.Component {
     return this.state.validating
     return this.state.validating
   }
   }
 
 
-  // if azure - isFinal
-  findInvalidFields(invalidFields, schemaRoot, isFinal) {
-    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)
-        }
-      } else if (isFinal && field.name === 'cloud_profile' && this.state.endpoint.cloud_profile === 'CustomCloud') {
-        this.findInvalidFields(invalidFields, field.custom_cloud_fields)
-      }
-    })
-  }
-
-  highlightRequired(isFinal) {
-    let invalidFields = []
-    this.findInvalidFields(invalidFields, this.props.providerStore.connectionInfoSchema, isFinal)
+  highlightRequired() {
+    let invalidFields = this.contentPluginRef.findInvalidFields()
     this.setState({ invalidFields })
     this.setState({ invalidFields })
     return invalidFields.length > 0
     return invalidFields.length > 0
   }
   }
@@ -323,14 +209,12 @@ class Endpoint extends React.Component {
     })
     })
   }
   }
 
 
-  handleFieldChange(field, value, parentGroup) {
+  handleFieldsChange(items) {
     let endpoint = { ...this.state.endpoint }
     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 })
     this.setState({ endpoint })
   }
   }
@@ -371,101 +255,6 @@ class Endpoint extends React.Component {
     this.props.onCancelClick()
     this.props.onCancelClick()
   }
   }
 
 
-  // if azure
-  handleNextClick() {
-    if (!this.highlightRequired()) {
-      this.setState({ showAdditionalConfig: true })
-    } else {
-      NotificationActions.notify('Please fill all the required fields', 'error')
-    }
-  }
-
-  // if azure
-  handleMasJsonConfigChange(value) {
-    this.setState({ masJsonConfig: value })
-
-    // JSON parse
-    let json
-    try {
-      json = JSON.parse(value)
-    } catch (e) {
-      return
-    }
-
-    if (!json.endpoints || !json.suffixes) {
-      return
-    }
-
-    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',
-    }
-
-    let endpoint = this.state.endpoint
-    const setValue = (object, key) => {
-      if (object[key]) {
-        endpoint[fieldNameMapper[key]] = object[key]
-      }
-    }
-    Object.keys(json.endpoints).forEach(k => {
-      setValue(json.endpoints, k)
-    })
-    Object.keys(json.suffixes).forEach(k => {
-      setValue(json.suffixes, k)
-    })
-
-    this.setState({ endpoint })
-  }
-
-  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'}
-          highlight={this.state.invalidFields.findIndex(fn => fn === field.name) > -1}
-          value={this.getFieldValue(field, parentGroup)}
-          onChange={value => { this.handleFieldChange(field, value, parentGroup) }}
-        />
-      )
-    })
-
-    return renderedFields
-  }
-
   renderEndpointStatus() {
   renderEndpointStatus() {
     let validation = this.props.endpointStore.validation
     let validation = this.props.endpointStore.validation
     if (!this.isValidating() && !validation) {
     if (!this.isValidating() && !validation) {
@@ -506,135 +295,30 @@ class Endpoint extends React.Component {
     )
     )
   }
   }
 
 
-  // if azure
-  renderNextButton() {
-    if (!this.state.endpoint.cloud_profile || this.state.endpoint.cloud_profile !== 'CustomCloud' || this.state.showAdditionalConfig) {
-      return null
-    }
-
-    return <Button large onClick={() => this.handleNextClick()}>Next</Button>
-  }
-
-  renderActionButton() {
-    let nextButton = this.renderNextButton()
-    if (nextButton) {
-      return nextButton
-    }
-
-    let button = <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) {
-        message = 'Saving ...'
-      }
-
-      button = <LoadingButton large>{message}</LoadingButton>
-    }
-
-    return button
-  }
-
-  renderCancelButton() {
-    // if azure
-    if (this.state.showAdditionalConfig) {
-      return <Button large secondary onClick={() => { this.setState({ showAdditionalConfig: false }) }}>Back</Button>
-    }
-
-    return <Button large secondary onClick={() => { this.handleCancelClick() }}>{this.props.cancelButtonText}</Button>
-  }
-
-  // if azure
-  renderAdditionalConfigLabel() {
-    if (!this.state.endpoint.cloud_profile || this.state.endpoint.cloud_profile !== 'CustomCloud') {
-      return null
-    }
-
-    return <ConfigLabel>* Additional configuration required</ConfigLabel>
-  }
-
   renderContent() {
   renderContent() {
-    // if azure
-    if (this.props.providerStore.connectionSchemaLoading || this.state.showAdditionalConfig) {
+    if (this.props.providerStore.connectionSchemaLoading) {
       return null
       return null
     }
     }
 
 
     return (
     return (
       <Content>
       <Content>
         {this.renderEndpointStatus()}
         {this.renderEndpointStatus()}
-        <Fields>
-          {this.renderFields(this.props.providerStore.connectionInfoSchema)}
-        </Fields>
-        {this.renderAdditionalConfigLabel()}
-        <Buttons>
-          {this.renderCancelButton()}
-          {this.renderActionButton()}
-        </Buttons>
-        <Tooltip />
-        {Tooltip.rebuild()}
-      </Content>
-    )
-  }
-
-  // if azure
-  renderAdditionalConfigContent() {
-    if (!this.state.showAdditionalConfig) {
-      return null
-    }
-
-    let fieldsContent = null
-
-    if (this.state.additionConfigManualInput) {
-      fieldsContent = (
-        <Fields>
-          {this.renderFields(this.props.providerStore.connectionInfoSchema.find(f => f.name === 'cloud_profile').custom_cloud_fields)}
-        </Fields>
-      )
-    } else {
-      fieldsContent = (
-        <PasteField>
-          <PasteFieldLabel>Azure Stack Profile Configuration</PasteFieldLabel>
-          <PasteFieldInput>
-            <TextArea
-              width="100%"
-              height="164px"
-              placeholder="Paste JSON output here"
-              value={this.state.masJsonConfig}
-              onChange={e => { this.handleMasJsonConfigChange(e.target.value) }}
-            />
-          </PasteFieldInput>
-        </PasteField>
-      )
-    }
-
-    let title = <AdditionalConfigTitle>Azure Stack Additional Configuration</AdditionalConfigTitle>
-    if (this.isValidating() || this.props.endpointStore.validation) {
-      title = null
-    }
-
-    return (
-      <Content>
-        {title}
-        {this.renderEndpointStatus()}
-        <AzureConfigInputType>
-          <RadioInput
-            checked={!this.state.additionConfigManualInput}
-            label="Paste Configuration"
-            onChange={e => { this.setState({ additionConfigManualInput: !e.target.checked }) }}
-          />
-          <RadioInput
-            checked={this.state.additionConfigManualInput}
-            label="Manual Input"
-            onChange={e => { this.setState({ additionConfigManualInput: e.target.checked }) }}
-          />
-        </AzureConfigInputType>
-        {fieldsContent}
-        <Buttons>
-          {this.renderCancelButton()}
-          {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() },
+          onRef: ref => { this.contentPluginRef = ref },
+          onResizeUpdate: () => { this.props.onResizeUpdate() },
+        })}
         <Tooltip />
         <Tooltip />
         {Tooltip.rebuild()}
         {Tooltip.rebuild()}
       </Content>
       </Content>
@@ -666,7 +350,6 @@ class Endpoint extends React.Component {
       <Wrapper>
       <Wrapper>
         <EndpointLogos style={{ marginBottom: '16px' }} height={128} endpoint={this.getEndpointType()} />
         <EndpointLogos style={{ marginBottom: '16px' }} height={128} endpoint={this.getEndpointType()} />
         {this.renderContent()}
         {this.renderContent()}
-        {this.renderAdditionalConfigContent()}
         {this.renderLoading()}
         {this.renderLoading()}
       </Wrapper>
       </Wrapper>
     )
     )

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

@@ -0,0 +1,397 @@
+/*
+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 NotificationActions from '../../../actions/NotificationActions'
+import Palette from '../../../components/styleUtils/Palette'
+import StyleProps from '../../../components/styleUtils/StyleProps'
+import {
+  EndpointField,
+  Button,
+  LoadingButton,
+  TextArea,
+  RadioInput,
+} from '../../../components'
+
+const Wrapper = styled.div``
+const Fields = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+  margin-left: -64px;
+  margin-top: 32px;
+  position: relative;
+`
+const FieldStyled = styled(EndpointField) `
+  margin-left: 64px;
+  min-width: 224px;
+  max-width: 224px;
+  margin-bottom: 16px;
+`
+const RadioGroup = styled.div`
+  width: 100%;
+`
+const ConfigLabel = styled.div`
+  font-size: 11px;
+  margin-top: -10px;
+  color: ${Palette.grayscale[3]};
+  position: absolute;
+  bottom: -4px;
+  left: 64px;
+`
+const Buttons = styled.div`
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+  margin-top: 32px;
+`
+const CustomConfigWrapper = styled.div`
+  width: 100%;
+`
+const CustomConfigTitle = styled.div`
+  text-align: center;
+`
+const CustomInputType = styled.div`
+  margin-top: 32px;
+  > div {
+    margin-bottom: 16px;
+  }
+`
+const PasteField = styled.div`
+  margin-top: 32px;
+`
+const PasteFieldLabel = styled.div`
+  font-size: 10px;
+  font-weight: ${StyleProps.fontWeights.medium};
+  color: ${Palette.grayscale[3]};
+  text-transform: uppercase;
+  margin-bottom: 4px;
+`
+const PasteFieldInput = styled.div``
+
+const Pages = {
+  main: 'main',
+  custom: 'custom',
+}
+const CustomTypes = {
+  manual: 'manual',
+  json: 'json',
+}
+
+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,
+  }
+
+  constructor() {
+    super()
+
+    this.state = {
+      currentPage: Pages.main,
+      customType: CustomTypes.json,
+      jsonConfig: '',
+    }
+  }
+
+  componentDidMount() {
+    this.props.onRef(this)
+  }
+
+  componentDidUpdate(prevProps, prevState) {
+    if (prevState.customType !== this.state.customType || prevState.currentPage !== this.state.currentPage) {
+      this.props.onResizeUpdate()
+    }
+  }
+
+  componentWillUnmount() {
+    this.props.onRef(undefined)
+  }
+
+  handleNextClick() {
+    if (!this.props.highlightRequired()) {
+      this.setState({ currentPage: Pages.custom })
+    } else {
+      NotificationActions.notify('Please fill all the required fields', 'error')
+    }
+  }
+
+  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.state.currentPage === Pages.custom) {
+      let customCloudFields = this.props.connectionInfoSchema.find(f => f.name === 'cloud_profile').custom_cloud_fields
+      find(customCloudFields)
+    }
+
+    return invalidFields
+  }
+
+  handleJsonConfigChange(value) {
+    this.setState({ jsonConfig: value })
+
+    let json
+    try {
+      json = JSON.parse(value)
+    } catch (e) {
+      return
+    }
+
+    if (!json.endpoints || !json.suffixes) {
+      return
+    }
+
+    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',
+    }
+
+    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)
+  }
+
+  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.props.handleFieldChange(field, value) }}
+        {...customProps}
+      />
+    )
+  }
+
+  renderCustomPage() {
+    if (this.state.currentPage !== Pages.custom) {
+      return null
+    }
+
+    let fields = null
+
+    if (this.state.customType === CustomTypes.manual) {
+      fields = (
+        <Fields>
+          {this.props.connectionInfoSchema.find(f => f.name === 'cloud_profile').custom_cloud_fields.map(field =>
+            this.renderField(field)
+          )}
+        </Fields>
+      )
+    } else {
+      fields = (
+        <PasteField>
+          <PasteFieldLabel>Azure Stack Profile Configuration JSON</PasteFieldLabel>
+          <PasteFieldInput>
+            <TextArea
+              width="100%"
+              height="164px"
+              placeholder="Paste JSON output here"
+              value={this.state.jsonConfig}
+              onChange={e => { this.handleJsonConfigChange(e.target.value) }}
+            />
+          </PasteFieldInput>
+        </PasteField>
+      )
+    }
+
+    let title = <CustomConfigTitle>Azure Stack Additional Configuration</CustomConfigTitle>
+    if (this.props.validating || this.props.validation) {
+      title = null
+    }
+
+    return (
+      <CustomConfigWrapper>
+        {title}
+        <CustomInputType>
+          <RadioInput
+            checked={this.state.customType === CustomTypes.json}
+            label="Paste Configuration"
+            onChange={e => { if (e.target.checked) this.setState({ customType: CustomTypes.json }) }}
+          />
+          <RadioInput
+            checked={this.state.customType === CustomTypes.manual}
+            label="Manual Input"
+            onChange={e => { if (e.target.checked) this.setState({ customType: CustomTypes.manual }) }}
+          />
+        </CustomInputType>
+        {fields}
+      </CustomConfigWrapper>
+    )
+  }
+
+  renderMainPage() {
+    if (this.state.currentPage === Pages.custom) {
+      return null
+    }
+
+    const fields = this.props.connectionInfoSchema
+
+    let renderedFields = fields.filter(f => f.name !== 'login_type' && f.name !== 'cloud_profile').map(field =>
+      this.renderField(field)
+    )
+
+    let loginTypeField = fields.find(f => f.name === 'login_type')
+
+    renderedFields.push((
+      <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>
+    ))
+
+    renderedFields = renderedFields.concat(loginTypeField.items.find(f => f.name === this.props.getFieldValue(loginTypeField)).fields.map(field =>
+      this.renderField(field)
+    ))
+
+    renderedFields.push(this.renderField(fields.find(f => f.name === 'cloud_profile')))
+
+    return (
+      <Fields>
+        {renderedFields}
+        {this.renderAdditionalConfigLabel()}
+      </Fields>
+    )
+  }
+
+  renderAdditionalConfigLabel() {
+    const fields = this.props.connectionInfoSchema
+
+    if (fields.length === 0 ||
+      this.props.getFieldValue(this.props.connectionInfoSchema.find(f => f.name === 'cloud_profile')) !== 'CustomCloud') {
+      return null
+    }
+
+    return <ConfigLabel>* Additional configuration required</ConfigLabel>
+  }
+
+  renderActionButton() {
+    let cloudProfileField = this.props.connectionInfoSchema.find(f => f.name === 'cloud_profile')
+
+    if (this.props.getFieldValue(cloudProfileField) === 'CustomCloud' && this.state.currentPage === Pages.main) {
+      return <Button large onClick={() => this.handleNextClick()}>Next</Button>
+    }
+
+    let actionButton = <Button large onClick={() => this.props.handleValidateClick()}>Validate and save</Button>
+
+    let message = 'Validating Endpoint ...'
+    if (this.props.validating || (this.props.validation && this.props.validation.valid)) {
+      if (this.props.validation && this.props.validation.valid) {
+        message = 'Saving ...'
+      }
+
+      actionButton = <LoadingButton large>{message}</LoadingButton>
+    }
+
+    return actionButton
+  }
+
+  renderCancelButton() {
+    let cancelButton
+
+    if (this.state.currentPage === Pages.main) {
+      cancelButton = <Button large secondary onClick={() => { this.props.handleCancelClick() }}>{this.props.cancelButtonText}</Button>
+    } else {
+      cancelButton = <Button large secondary onClick={() => { this.setState({ currentPage: Pages.main }) }}>Back</Button>
+    }
+
+    return cancelButton
+  }
+
+  renderButtons() {
+    return (
+      <Buttons>
+        {this.renderCancelButton()}
+        {this.renderActionButton()}
+      </Buttons>
+    )
+  }
+
+  render() {
+    const fields = this.props.connectionInfoSchema
+    if (fields.length === 0) {
+      return null
+    }
+
+    return (
+      <Wrapper>
+        {this.renderMainPage()}
+        {this.renderCustomPage()}
+        {this.renderButtons()}
+      </Wrapper>
+    )
+  }
+}
+
+export default ContentPlugin

+ 3 - 2
src/plugins/endpoint/azure/ConnectionSchemaParser.js → src/plugins/endpoint/azure/SchemaPlugin.js

@@ -17,7 +17,7 @@ import {
   defaultSchemaToFields,
   defaultSchemaToFields,
   generateField,
   generateField,
   fieldsToPayload,
   fieldsToPayload,
-} from '../default/ConnectionSchemaParser'
+} from '../default/SchemaPlugin'
 
 
 const azureConnectionParse = schema => {
 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 commonFields = connectionSchemaToFields(schema).filter(f => f.type !== 'object' && f.name !== 'secret_ref' && Object.keys(f).findIndex(k => k === 'enum') === -1)
@@ -73,7 +73,8 @@ export default class ConnectionSchemaParser {
     payload.description = data.description
     payload.description = data.description
 
 
     let connectionInfo = fieldsToPayload(data, schema)
     let connectionInfo = fieldsToPayload(data, schema)
-    connectionInfo[data.login_type] = fieldsToPayload(data, schema.properties[data.login_type])
+    let loginType = data.login_type || 'user_credentials'
+    connectionInfo[loginType] = fieldsToPayload(data, schema.properties[loginType])
 
 
     if (data.cloud_profile === 'CustomCloud') {
     if (data.cloud_profile === 'CustomCloud') {
       connectionInfo.custom_cloud_properties = {
       connectionInfo.custom_cloud_properties = {

+ 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, Button, LoadingButton } from '../../../components'
+
+const Wrapper = styled.div``
+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 Buttons = styled.div`
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+  margin-top: 32px;
+`
+
+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 renderedFields = this.props.connectionInfoSchema.map(field => (
+      <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.props.handleFieldChange(field, value) }}
+      />
+    ))
+
+    return (
+      <Fields>
+        {renderedFields}
+      </Fields>
+    )
+  }
+
+  renderButtons() {
+    let actionButton = <Button large onClick={() => this.props.handleValidateClick()}>Validate and save</Button>
+
+    let message = 'Validating Endpoint ...'
+    if (this.props.validating || (this.props.validation && this.props.validation.valid)) {
+      if (this.props.validation && this.props.validation.valid) {
+        message = 'Saving ...'
+      }
+
+      actionButton = <LoadingButton large>{message}</LoadingButton>
+    }
+
+    return (
+      <Buttons>
+        <Button large secondary onClick={() => { this.props.handleCancelClick() }}>{this.props.cancelButtonText}</Button>
+        {actionButton}
+      </Buttons>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {this.renderFields()}
+        {this.renderButtons()}
+      </Wrapper>
+    )
+  }
+}
+
+export default ContentPlugin

+ 0 - 30
src/plugins/endpoint/default/ContentRenderer.jsx

@@ -1,30 +0,0 @@
-/*
-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'
-
-const Wrapper = styled.div``
-
-class ContentRenderer extends React.Component {
-  render() {
-    return (
-      <Wrapper>
-        ContentRenderer
-      </Wrapper>
-    )
-  }
-}
-
-export default ContentRenderer

+ 0 - 0
src/plugins/endpoint/default/ConnectionSchemaParser.js → src/plugins/endpoint/default/SchemaPlugin.js


+ 10 - 8
src/plugins/endpoint/index.js

@@ -12,15 +12,17 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import DefaultSchemaParser from './default/ConnectionSchemaParser'
-import AzureSchemaParser from './azure/ConnectionSchemaParser'
-import DefaultContentRenderer from './default/ContentRenderer'
+import DefaultSchemaPlugin from './default/SchemaPlugin'
+import AzureSchemaPlugin from './azure/SchemaPlugin'
+import DefaultContentPlugin from './default/ContentPlugin'
+import AzureContentPlugin from './azure/ContentPlugin'
 
 
-export const ConnectionSchemaParsers = {
-  default: DefaultSchemaParser,
-  azure: AzureSchemaParser,
+export const SchemaPlugin = {
+  default: DefaultSchemaPlugin,
+  azure: AzureSchemaPlugin,
 }
 }
 
 
-export const ContentRenderer = {
-  default: DefaultContentRenderer,
+export const ContentPlugin = {
+  default: DefaultContentPlugin,
+  azure: AzureContentPlugin,
 }
 }

+ 4 - 4
src/sources/Schemas.js

@@ -12,8 +12,8 @@ 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/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import { ConnectionSchemaParsers } from '../plugins/endpoint'
-import { defaultSchemaToFields } from '../plugins/endpoint/default/ConnectionSchemaParser'
+import { SchemaPlugin } from '../plugins/endpoint'
+import { defaultSchemaToFields } from '../plugins/endpoint/default/SchemaPlugin'
 
 
 class SchemaParser {
 class SchemaParser {
   static storedConnectionsSchemas = {}
   static storedConnectionsSchemas = {}
@@ -23,7 +23,7 @@ class SchemaParser {
       this.storedConnectionsSchemas[provider] = schema
       this.storedConnectionsSchemas[provider] = schema
     }
     }
 
 
-    let parsers = ConnectionSchemaParsers[provider] || ConnectionSchemaParsers.default
+    let parsers = SchemaPlugin[provider] || SchemaPlugin.default
     let fields = parsers.parseSchemaToFields(schema)
     let fields = parsers.parseSchemaToFields(schema)
 
 
     return fields
     return fields
@@ -47,7 +47,7 @@ class SchemaParser {
 
 
   static fieldsToPayload(data) {
   static fieldsToPayload(data) {
     let storedSchema = this.storedConnectionsSchemas[data.type] || this.storedConnectionsSchemas.general
     let storedSchema = this.storedConnectionsSchemas[data.type] || this.storedConnectionsSchemas.general
-    let parsers = ConnectionSchemaParsers[data.type] || ConnectionSchemaParsers.default
+    let parsers = SchemaPlugin[data.type] || SchemaPlugin.default
     let payload = parsers.parseFieldsToPayload(data, storedSchema)
     let payload = parsers.parseFieldsToPayload(data, storedSchema)
 
 
     return payload
     return payload

+ 5 - 0
src/stores/ProviderStore.js

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