Bladeren bron

Merge pull request #231 from smiclea/oci

Add OCI provider support
Dorin Paslaru 8 jaren geleden
bovenliggende
commit
964d19db22
34 gewijzigde bestanden met toevoegingen van 284 en 58 verwijderingen
  1. 1 1
      src/components/atoms/CopyButton/index.jsx
  2. 14 8
      src/components/atoms/DropdownButton/index.jsx
  3. 7 0
      src/components/atoms/EndpointLogos/images/oci-128-disabled.svg
  4. 7 0
      src/components/atoms/EndpointLogos/images/oci-128.svg
  5. 7 0
      src/components/atoms/EndpointLogos/images/oci-32.svg
  6. 7 0
      src/components/atoms/EndpointLogos/images/oci-42.svg
  7. 7 0
      src/components/atoms/EndpointLogos/images/oci-64.svg
  8. 14 2
      src/components/atoms/EndpointLogos/index.jsx
  9. 3 0
      src/components/molecules/Dropdown/index.jsx
  10. 19 0
      src/components/molecules/EndpointField/index.jsx
  11. 9 2
      src/components/molecules/PropertiesTable/index.jsx
  12. 1 0
      src/components/molecules/PropertiesTable/test.jsx
  13. 4 2
      src/components/molecules/Table/index.jsx
  14. 1 1
      src/components/molecules/WizardOptionsField/index.jsx
  15. 5 5
      src/components/organisms/Endpoint/index.jsx
  16. 6 2
      src/components/organisms/MainDetails/index.jsx
  17. 1 1
      src/components/organisms/WizardOptions/index.jsx
  18. 1 0
      src/components/organisms/WizardOptions/test.jsx
  19. 24 6
      src/components/pages/EndpointsPage/index.jsx
  20. 17 1
      src/components/pages/WizardPage/index.jsx
  21. 10 2
      src/config.js
  22. 4 4
      src/plugins/endpoint/default/ContentPlugin.jsx
  23. 2 0
      src/plugins/endpoint/index.js
  24. 37 0
      src/plugins/endpoint/oci/SchemaPlugin.js
  25. 5 1
      src/sources/EndpointSource.js
  26. 6 2
      src/sources/ProviderSource.js
  27. 8 1
      src/sources/WizardSource.js
  28. 1 1
      src/stores/EndpointStore.js
  29. 38 8
      src/stores/ProviderStore.js
  30. 3 2
      src/types/Endpoint.js
  31. 3 1
      src/types/Field.js
  32. 6 5
      src/utils/ApiCaller.js
  33. 2 0
      src/utils/LabelDictionary.js
  34. 4 0
      src/utils/ObjectUtils.js

+ 1 - 1
src/components/atoms/CopyButton/index.jsx

@@ -24,7 +24,7 @@ import copyImage from './images/copy.svg'
 
 const Wrapper = styled.span`
   opacity: 0;
-  width: 16px;
+  min-width: 16px;
   height: 16px;
   display: inline-block;
   background: url('${copyImage}') no-repeat;

+ 14 - 8
src/components/atoms/DropdownButton/index.jsx

@@ -15,7 +15,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 // @flow
 
 import React from 'react'
-import styled from 'styled-components'
+import styled, { css } from 'styled-components'
 
 import arrowImage from './images/arrow.js'
 
@@ -97,15 +97,20 @@ const Wrapper = styled.div`
   cursor: ${props => props.disabled ? 'default' : 'pointer'};
   transition: all ${StyleProps.animations.swift};
   background: ${props => getBackgroundColor(props)};
+  ${props => props.embedded ? css`
+    border: 0;
+    width: calc(100% + 8px);
+    margin-left: -16px;
+  ` : ''}
 
   #dropdown-arrow-image {stroke: ${props => getArrowColor(props)};}
   &:hover {
-    #dropdown-arrow-image {stroke: ${props => props.disabled ? '' : 'white'};}
-    background: ${props => props.disabled ? '' : Palette.primary};
+    #dropdown-arrow-image {stroke: ${props => props.disabled ? '' : props.embedded ? '' : 'white'};}
+    background: ${props => props.disabled ? '' : props.embedded ? '' : Palette.primary};
   }
 
   &:hover ${Label} {
-    color: ${props => props.disabled ? '' : 'white'};
+    color: ${props => props.disabled ? '' : props.embedded ? '' : 'white'};
   }
 `
 const Arrow = styled.div`
@@ -122,6 +127,7 @@ type Props = {
   className?: string,
   disabled?: boolean,
   'data-test-id'?: string,
+  embedded?: boolean,
 }
 class DropdownButton extends React.Component<Props> {
   render() {
@@ -140,15 +146,15 @@ class DropdownButton extends React.Component<Props> {
       >
         <Label
           {...this.props}
-          onClick={() => {}}
-          innerRef={() => {}}
+          onClick={() => { }}
+          innerRef={() => { }}
           data-test-id="dropdownButton-value"
           disabled={this.props.disabled}
         >{this.props.value}</Label>
         <Arrow
           {...this.props}
-          innerRef={() => {}}
-          onClick={() => {}}
+          innerRef={() => { }}
+          onClick={() => { }}
           data-test-id=""
           disabled={this.props.disabled}
           dangerouslySetInnerHTML={{ __html: arrowImage }}

File diff suppressed because it is too large
+ 7 - 0
src/components/atoms/EndpointLogos/images/oci-128-disabled.svg


File diff suppressed because it is too large
+ 7 - 0
src/components/atoms/EndpointLogos/images/oci-128.svg


File diff suppressed because it is too large
+ 7 - 0
src/components/atoms/EndpointLogos/images/oci-32.svg


File diff suppressed because it is too large
+ 7 - 0
src/components/atoms/EndpointLogos/images/oci-42.svg


File diff suppressed because it is too large
+ 7 - 0
src/components/atoms/EndpointLogos/images/oci-64.svg


+ 14 - 2
src/components/atoms/EndpointLogos/index.jsx

@@ -26,6 +26,7 @@ import opc32Image from './images/opc-32.svg'
 import openstack32Image from './images/openstack-32.svg'
 import oraclevm32Image from './images/oraclevm-32.svg'
 import vmware32Image from './images/vmware-32.svg'
+import oci32Image from './images/oci-32.svg'
 
 import aws42Image from './images/aws-42.svg'
 import azure42Image from './images/azure-42.svg'
@@ -33,6 +34,7 @@ import opc42Image from './images/opc-42.svg'
 import openstack42Image from './images/openstack-42.svg'
 import oraclevm42Image from './images/oraclevm-42.svg'
 import vmware42Image from './images/vmware-42.svg'
+import oci42Image from './images/oci-42.svg'
 
 import aws64Image from './images/aws-64.svg'
 import azure64Image from './images/azure-64.svg'
@@ -40,6 +42,7 @@ import opc64Image from './images/opc-64.svg'
 import openstack64Image from './images/openstack-64.svg'
 import oraclevm64Image from './images/oraclevm-64.svg'
 import vmware64Image from './images/vmware-64.svg'
+import oci64Image from './images/oci-64.svg'
 
 import aws128Image from './images/aws-128.svg'
 import azure128Image from './images/azure-128.svg'
@@ -47,6 +50,7 @@ import opc128Image from './images/opc-128.svg'
 import openstack128Image from './images/openstack-128.svg'
 import oraclevm128Image from './images/oraclevm-128.svg'
 import vmware128Image from './images/vmware-128.svg'
+import oci128Image from './images/oci-128.svg'
 
 import aws128DisabledImage from './images/aws-128-disabled.svg'
 import azure128DisabledImage from './images/azure-128-disabled.svg'
@@ -54,6 +58,7 @@ import opc128DisabledImage from './images/opc-128-disabled.svg'
 import openstack128DisabledImage from './images/openstack-128-disabled.svg'
 import oraclevm128DisabledImage from './images/oraclevm-128-disabled.svg'
 import vmware128DisabledImage from './images/vmware-128-disabled.svg'
+import oci128DisabledImage from './images/oci-128-disabled.svg'
 
 const endpointImages = {
   azure: [
@@ -98,6 +103,13 @@ const endpointImages = {
     { h: 128, image: aws128Image },
     { h: 128, image: aws128DisabledImage, disabled: true },
   ],
+  oci: [
+    { h: 32, image: oci32Image },
+    { h: 42, image: oci42Image },
+    { h: 64, image: oci64Image },
+    { h: 128, image: oci128Image },
+    { h: 128, image: oci128DisabledImage, disabled: true },
+  ],
 }
 const Wrapper = styled.div``
 const Logo = styled.div`
@@ -127,7 +139,7 @@ class EndpointLogos extends React.Component<Props> {
     height: 64,
   }
 
-  renderLogo(size: {w: number, h: number}) {
+  renderLogo(size: { w: number, h: number }) {
     let imageInfo = null
 
     if (this.props.endpoint && endpointImages[this.props.endpoint]) {
@@ -149,7 +161,7 @@ class EndpointLogos extends React.Component<Props> {
     )
   }
 
-  renderGenericLogo(size: {w: number, h: number}) {
+  renderGenericLogo(size: { w: number, h: number }) {
     return (
       <Generic
         data-test-id="endpointLogos-genericLogo"

+ 3 - 0
src/components/molecules/Dropdown/index.jsx

@@ -38,6 +38,7 @@ const getWidth = props => {
 }
 const Wrapper = styled.div`
   position: relative;
+  ${props => props.embedded ? 'width: 100%;' : ''}
 `
 const List = styled.div`
   position: absolute;
@@ -111,6 +112,7 @@ type Props = {
   disabled: boolean,
   width: number,
   'data-test-id'?: string,
+  embedded?: boolean,
 }
 type State = {
   showDropdownList: boolean,
@@ -325,6 +327,7 @@ class Dropdown extends React.Component<Props, State> {
         onMouseDown={() => { this.itemMouseDown = true }}
         onMouseUp={() => { this.itemMouseDown = false }}
         data-test-id={this.props['data-test-id'] || 'dropdown'}
+        embedded={this.props.embedded}
       >
         <DropdownButton
           {...this.props}

+ 19 - 0
src/components/molecules/EndpointField/index.jsx

@@ -24,6 +24,7 @@ import RadioInput from '../../atoms/RadioInput'
 import InfoIcon from '../../atoms/InfoIcon'
 import Dropdown from '../../molecules/Dropdown'
 import DropdownInput from '../../molecules/DropdownInput'
+import TextArea from '../../atoms/TextArea'
 import type { Field as FieldType } from '../../../types/Field'
 
 import LabelDictionary from '../../../utils/LabelDictionary'
@@ -59,6 +60,7 @@ type Props = {
   disabled: boolean,
   enum?: string[],
   items?: FieldType[],
+  useTextArea?: boolean,
 }
 @observer
 class Field extends React.Component<Props> {
@@ -89,6 +91,20 @@ class Field extends React.Component<Props> {
     )
   }
 
+  renderTextArea() {
+    return (
+      <TextArea
+        style={{ width: '100%' }}
+        required={this.props.required}
+        highlight={this.props.highlight}
+        value={this.props.value}
+        onChange={e => { if (this.props.onChange) this.props.onChange(e.target.value) }}
+        placeholder={LabelDictionary.get(this.props.name)}
+        disabled={this.props.disabled}
+      />
+    )
+  }
+
   renderEnumDropdown() {
     if (!this.props.enum) {
       return null
@@ -186,6 +202,9 @@ class Field extends React.Component<Props> {
         if (this.props.enum) {
           return this.renderEnumDropdown()
         }
+        if (this.props.useTextArea) {
+          return this.renderTextArea()
+        }
         return this.renderTextInput()
       case 'integer':
         if (this.props.minimum || this.props.maximum) {

+ 9 - 2
src/components/molecules/PropertiesTable/index.jsx

@@ -105,9 +105,15 @@ class PropertiesTable extends React.Component<Props> {
       return null
     }
     let items = prop.enum.map(e => {
+      if (typeof e === 'string') {
+        return {
+          label: this.getName(e),
+          value: e,
+        }
+      }
       return {
-        label: this.getName(e),
-        value: e,
+        label: e.name,
+        value: e.id,
       }
     })
 
@@ -120,6 +126,7 @@ class PropertiesTable extends React.Component<Props> {
 
     return (
       <Dropdown
+        embedded
         data-test-id={`${baseId}-dropdown-${prop.name}`}
         width={320}
         noSelectionMessage="Choose a value"

+ 1 - 0
src/components/molecules/PropertiesTable/test.jsx

@@ -26,6 +26,7 @@ let properties = [
   { type: 'strict-boolean', name: 'prop_2', label: 'Strict Boolean', value: false },
   { type: 'string', name: 'prop_3', label: 'String', value: 'value-3' },
   { type: 'string', name: 'prop_3a', label: 'String', required: true, value: 'value-4' },
+  // $FlowIgnore
   { type: 'string', enum: ['a', 'b', 'c'], name: 'prop_4', label: 'String enum', value: 'value-5' },
 ]
 const valueCallback = prop => {

+ 4 - 2
src/components/molecules/Table/index.jsx

@@ -86,7 +86,9 @@ const Row = styled.div`
 const RowData = styled.div`
   ${props => TableData(props)}
   color: ${Palette.grayscale[4]};
-  ${props => props.customStyle}
+  ${props => props.customStyle};
+  overflow: hidden;
+  text-overflow: ellipsis;
 `
 const NoItems = styled.div`
   text-align: center;
@@ -144,7 +146,7 @@ class Table extends React.Component<Props> {
       return null
     }
 
-    let dataWidth = `${100 / this.props.items.length}%`
+    let dataWidth = `${100 / this.props.header.length}%`
     return (
       <Body customStyle={this.props.bodyStyle}>
         {this.props.items.map((row, i) => {

+ 1 - 1
src/components/molecules/WizardOptionsField/index.jsx

@@ -110,7 +110,7 @@ class WizardOptionsField extends React.Component<Props> {
   renderEnumDropdown() {
     let items = this.props.enum.map(e => {
       return {
-        label: typeof e === 'string' ? LabelDictionary.get(e) : e.name,
+        label: typeof e === 'string' ? e : e.name,
         value: typeof e === 'string' ? e : e.id,
       }
     })

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

@@ -372,6 +372,11 @@ class Endpoint extends React.Component<Props, State> {
     }
     return (
       <Content>
+        {/* Fix browsers autofilling password fields */}
+        <div style={{ position: 'absolute', left: '-10000px' }}>
+          <input name="username" type="text" />
+          <input name="password" type="password" />
+        </div>
         {this.renderEndpointStatus()}
         {React.createElement(ContentPlugin[endpointType] || ContentPlugin.default, {
           connectionInfoSchema: providerStore.connectionInfoSchema,
@@ -394,11 +399,6 @@ class Endpoint extends React.Component<Props, State> {
         {this.renderButtons()}
         <Tooltip />
         {Tooltip.rebuild()}
-        {/* Fix browsers autofilling password fields */}
-        <div style={{ position: 'absolute', left: '-10000px' }}>
-          <input type="text" />
-          <input type="password" />
-        </div>
       </Content>
     )
   }

+ 6 - 2
src/components/organisms/MainDetails/index.jsx

@@ -98,6 +98,9 @@ const PropertyRow = styled.div`
 const PropertyText = css``
 const PropertyName = styled.div`
   ${PropertyText}
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 50%;
 `
 const PropertyValue = styled.div`
   ${PropertyText}
@@ -105,6 +108,7 @@ const PropertyValue = styled.div`
   text-align: right;
   overflow: hidden;
   text-overflow: ellipsis;
+  max-width: 50%;
 `
 
 type Props = {
@@ -266,7 +270,7 @@ class MainDetails extends React.Component<Props> {
           return (
             <PropertyRow key={prop.label}>
               <PropertyName>{prop.label}</PropertyName>
-              <PropertyValue>{prop.value}</PropertyValue>
+              <PropertyValue><CopyValue value={prop.value} /></PropertyValue>
             </PropertyRow>
           )
         })}
@@ -335,7 +339,7 @@ class MainDetails extends React.Component<Props> {
         <Column width="17.5%">
           <Arrow />
         </Column>
-        <Column width="auto" style={{ flexGrow: 1 }}>
+        <Column width="48%" style={{ flexGrow: 1 }}>
           <Row>
             <Field>
               <Label>Target</Label>

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

@@ -142,7 +142,7 @@ class WizardOptions extends React.Component<Props> {
     }
 
     let executeNowColumn
-    let fields = fieldsSchema.map((field, i) => {
+    let fields = fieldsSchema.filter(f => f.type !== 'object' || f.properties).map((field, i) => {
       let column = i % 2 === 0 ? 'left' : 'right'
       if (field.name === 'execute_now') {
         executeNowColumn = column

+ 1 - 0
src/components/organisms/WizardOptions/test.jsx

@@ -42,6 +42,7 @@ let fields = [
   {
     name: 'enum_field',
     type: 'string',
+    // $FlowIgnore
     enum: ['enum 1', 'enum 2', 'enum 3'],
   },
   {

+ 24 - 6
src/components/pages/EndpointsPage/index.jsx

@@ -38,6 +38,7 @@ import endpointStore from '../../../stores/EndpointStore'
 import migrationStore from '../../../stores/MigrationStore'
 import replicaStore from '../../../stores/ReplicaStore'
 import providerStore from '../../../stores/ProviderStore'
+import notificationStore from '../../../stores/NotificationStore'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import { requestPollTimeout } from '../../../config.js'
 import EndpointDuplicateOptions from '../../organisms/EndpointDuplicateOptions'
@@ -87,7 +88,7 @@ class EndpointsPage extends React.Component<{}, State> {
     projectStore.getProjects()
 
     this.stopPolling = false
-    this.pollData()
+    this.pollData(true)
   }
 
   componentWillUnmount() {
@@ -172,7 +173,11 @@ class EndpointsPage extends React.Component<{}, State> {
     let items = this.state.confirmationItems || []
     Promise.all(items.map(endpoint => {
       return EndpointSource.getConnectionInfo(endpoint).then(connectionInfo => {
-        endpoints.push({ ...endpoint, connection_info: connectionInfo })
+        endpoints.push({
+          ...endpoint,
+          connection_info: connectionInfo,
+          name: `${endpoint.name}${!switchProject ? ' (copy)' : ''}`,
+        })
       })
     })).then(() => {
       if (switchProject) {
@@ -184,10 +189,23 @@ class EndpointsPage extends React.Component<{}, State> {
     }).then(() => {
       return Promise.all(endpoints.map(endpoint => {
         return EndpointSource.add(endpoint, true)
-      }))
+      }).map(p => p.catch(e => e))).then(results => {
+        let internalServerErrors = results.filter(r => r.status && r.status === 500)
+        if (internalServerErrors.length > 0) {
+          notificationStore.notify(`There was a problem duplicating ${internalServerErrors.length} endpoint${internalServerErrors.length > 1 ? 's' : ''}`, 'error')
+        }
+        let forbiddenErrors = results.filter(r => r.status && r.status === 403)
+        if (forbiddenErrors.length > 0 && forbiddenErrors[0].data && forbiddenErrors[0].data.description) {
+          notificationStore.notify(String(forbiddenErrors[0].data.description), 'error')
+        }
+      })
+    }).catch(e => {
+      if (e.data && e.data.description) {
+        notificationStore.notify(e.data.description, 'error')
+      }
     }).then(() => {
+      this.pollData(true)
       this.setState({ showDuplicateModal: false, duplicating: false })
-      this.pollData()
     })
   }
 
@@ -238,12 +256,12 @@ class EndpointsPage extends React.Component<{}, State> {
     })
   }
 
-  pollData() {
+  pollData(showLoading?: boolean = false) {
     if (this.state.modalIsOpen || this.stopPolling) {
       return
     }
 
-    Promise.all([endpointStore.getEndpoints(), migrationStore.getMigrations(), replicaStore.getReplicas()]).then(() => {
+    Promise.all([endpointStore.getEndpoints({ showLoading }), migrationStore.getMigrations(), replicaStore.getReplicas()]).then(() => {
       this.pollTimeout = setTimeout(() => { this.pollData() }, requestPollTimeout)
     })
   }

+ 17 - 1
src/components/pages/WizardPage/index.jsx

@@ -34,7 +34,8 @@ import notificationStore from '../../../stores/NotificationStore'
 import scheduleStore from '../../../stores/ScheduleStore'
 import replicaStore from '../../../stores/ReplicaStore'
 import KeyboardManager from '../../../utils/KeyboardManager'
-import { wizardConfig, executionOptions } from '../../../config'
+import O from '../../../utils/ObjectUtils'
+import { wizardConfig, executionOptions, providersWithExtraOptions } from '../../../config'
 import type { MainItem } from '../../../types/MainItem'
 import type { Endpoint as EndpointType } from '../../../types/Endpoint'
 import type { Instance, Nic } from '../../../types/Instance'
@@ -238,6 +239,21 @@ class WizardPage extends React.Component<Props, State> {
   handleOptionsChange(field: Field, value: any) {
     wizardStore.updateData({ networks: null })
     wizardStore.updateOptions({ field, value })
+
+    let provider = wizardStore.data.target && wizardStore.data.target.type
+    let providerWithExtraOptions = providersWithExtraOptions.find(p => typeof p !== 'string' && p.name === provider)
+    if (provider && providerWithExtraOptions && typeof providerWithExtraOptions !== 'string' && providerWithExtraOptions.envRequestFields) {
+      let validFields = providerWithExtraOptions.envRequestFields.filter(fn => wizardStore.data.options ? O.isValid(wizardStore.data.options[fn]) : false)
+      if (
+        validFields.length === providerWithExtraOptions.envRequestFields.length &&
+        wizardStore.data.options &&
+        wizardStore.data.target &&
+        validFields.find(fn => fn === field.name)
+      ) {
+        providerStore.getDestinationOptions(wizardStore.data.target.id, provider, wizardStore.data.options)
+      }
+    }
+
     wizardStore.setPermalink(wizardStore.data)
   }
 

+ 10 - 2
src/config.js

@@ -81,7 +81,15 @@ export const wizardConfig = {
   instancesItemsPerPage: 6,
 }
 
-// Providers for which `destination-options` API call will be made
-export const providersWithExtraOptions = []
+// Providers for which `destination-options` API call(s) will be made
+// If item is an object and has `envRequestFields` array,
+// subsequent requests to `destination-options` will be made with those fields, if they are set
+export const providersWithExtraOptions = [
+  'openstack',
+  {
+    name: 'oci',
+    envRequestFields: ['compartment', 'availability_domain'],
+  },
+]
 
 export const basename = process.env.PUBLIC_PATH

+ 4 - 4
src/plugins/endpoint/default/ContentPlugin.jsx

@@ -31,8 +31,8 @@ export const Fields = styled.div`
   flex-direction: column;
   overflow: auto;
 `
-export const FieldStyled = styled(EndpointField)`
-  min-width: 224px;
+export const FieldStyled = styled(EndpointField) `
+  min-width: ${props => props.useTextArea ? '100%' : '224px'};
   max-width: 224px;
   margin-bottom: 16px;
 `
@@ -89,14 +89,14 @@ class ContentPlugin extends React.Component<Props> {
           onChange={value => { this.props.handleFieldChange(field, value) }}
         />
       )
-      if (i % 2 !== 0) {
+      if (i % 2 !== 0 && !field.useTextArea && !this.props.connectionInfoSchema[i - 1].useTextArea) {
         rows.push((
           <Row key={field.name}>
             {lastField}
             {currentField}
           </Row>
         ))
-      } else if (i === this.props.connectionInfoSchema.length - 1) {
+      } else if (field.useTextArea || i === this.props.connectionInfoSchema.length - 1) {
         rows.push((
           <Row key={field.name}>
             {currentField}

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

@@ -20,11 +20,13 @@ import DefaultContentPlugin from './default/ContentPlugin'
 import AzureContentPlugin from './azure/ContentPlugin'
 import OpenstackContentPlugin from './openstack/ContentPlugin'
 import OpenstackSchemaPlugin from './openstack/SchemaPlugin'
+import OciSchemaPlugin from './oci/SchemaPlugin'
 
 export const SchemaPlugin = {
   default: DefaultSchemaPlugin,
   azure: AzureSchemaPlugin,
   openstack: OpenstackSchemaPlugin,
+  oci: OciSchemaPlugin,
 }
 
 export const ContentPlugin = {

+ 37 - 0
src/plugins/endpoint/oci/SchemaPlugin.js

@@ -0,0 +1,37 @@
+/*
+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/>.
+*/
+
+// @flow
+
+import type { Schema } from '../../../types/Schema'
+import type { Field } from '../../../types/Field'
+
+import DefaultConnectionSchemaParser from '../default/SchemaPlugin'
+
+export default class ConnectionSchemaParser {
+  static parseSchemaToFields(schema: Schema): Field[] {
+    let fields = DefaultConnectionSchemaParser.parseSchemaToFields(schema)
+    let privateKeyField = fields.find(f => f.name === 'private_key_data')
+    if (privateKeyField) {
+      privateKeyField.useTextArea = true
+    }
+
+    return fields
+  }
+
+  static parseFieldsToPayload(data: { [string]: mixed }, schema: Schema) {
+    let payload = DefaultConnectionSchemaParser.parseFieldsToPayload(data, schema)
+    return payload
+  }
+}

+ 5 - 1
src/sources/EndpointSource.js

@@ -86,9 +86,13 @@ class EdnpointSource {
     let index = endpoint.connection_info.secret_ref && endpoint.connection_info.secret_ref.lastIndexOf('/')
     let uuid = index && endpoint.connection_info.secret_ref && endpoint.connection_info.secret_ref.substr(index + 1)
 
+    if (!uuid) {
+      return Promise.resolve(endpoint.connection_info)
+    }
+
     return new Promise((resolve, reject) => {
       Api.send({
-        url: `${servicesUrl.barbican}/v1/secrets/${uuid || ''}/payload`,
+        url: `${servicesUrl.barbican}/v1/secrets/${uuid || 'undefined'}/payload`,
         responseType: 'text',
         headers: { Accept: 'text/plain' },
       }).then((response) => {

+ 6 - 2
src/sources/ProviderSource.js

@@ -59,11 +59,15 @@ class ProviderSource {
     })
   }
 
-  static getDestinationOptions(endpointId: string): Promise<DestinationOption[]> {
+  static getDestinationOptions(endpointId: string, envData: ?{ [string]: mixed }): Promise<DestinationOption[]> {
     return new Promise((resolve, reject) => {
       let projectId = cookie.get('projectId')
+      let envString = ''
+      if (envData) {
+        envString = `?env=${btoa(JSON.stringify(envData))}`
+      }
 
-      Api.get(`${servicesUrl.coriolis}/${projectId || 'null'}/endpoints/${endpointId}/destination-options`).then(response => {
+      Api.get(`${servicesUrl.coriolis}/${projectId || 'null'}/endpoints/${endpointId}/destination-options${envString}`).then(response => {
         let options = response.data.destination_options
         resolve(options)
       }).catch(() => { reject() })

+ 8 - 1
src/sources/WizardSource.js

@@ -25,7 +25,7 @@ import type { MainItem } from '../types/MainItem'
 class WizardSourceUtils {
   static getDestinationEnv(data) {
     let env = {}
-    let specialOptions = ['execute_now', 'separate_vm', 'skip_os_morphing'].concat(executionOptions.map(o => o.name))
+    let specialOptions = ['execute_now', 'separate_vm', 'skip_os_morphing', 'windows_image', 'linux_image'].concat(executionOptions.map(o => o.name))
 
     if (data.options) {
       Object.keys(data.options).forEach(optionName => {
@@ -52,6 +52,13 @@ class WizardSourceUtils {
         env.network_map[mapping.sourceNic.network_name] = mapping.targetNetwork.id
       })
     }
+    env.migr_image_map = {}
+    if (data.options && data.options.windows_image) {
+      env.migr_image_map.windows = data.options.windows_image
+    }
+    if (data.options && data.options.linux_image) {
+      env.migr_image_map.linux = data.options.linux_image
+    }
 
     return env
   }

+ 1 - 1
src/stores/EndpointStore.js

@@ -38,7 +38,7 @@ class EndpointStore {
   @observable connectionsInfoLoading = false
 
   @action getEndpoints(options?: { showLoading: boolean }) {
-    if ((options && options.showLoading) || this.endpoints.length === 0) {
+    if (options && options.showLoading) {
       this.loading = true
     }
 

+ 38 - 8
src/stores/ProviderStore.js

@@ -32,6 +32,8 @@ class ProviderStore {
   @observable destinationOptions: DestinationOption[] = []
   @observable destinationOptionsLoading: boolean = false
 
+  lastOptionsSchemaType: string = ''
+
   @action getConnectionInfoSchema(providerName: string): Promise<void> {
     this.connectionSchemaLoading = true
 
@@ -61,6 +63,7 @@ class ProviderStore {
 
   @action loadOptionsSchema(providerName: string, schemaType: string): Promise<void> {
     this.optionsSchemaLoading = true
+    this.lastOptionsSchemaType = schemaType
 
     return ProviderSource.loadOptionsSchema(providerName, schemaType).then((fields: Field[]) => {
       this.optionsSchemaLoading = false
@@ -70,26 +73,53 @@ class ProviderStore {
     })
   }
 
-  @action getDestinationOptions(endpointId: string, provider: string): Promise<void> {
-    if (!providersWithExtraOptions.find(p => p === provider)) {
+  @action getDestinationOptions(endpointId: string, provider: string, envData?: { [string]: mixed }): Promise<void> {
+    let providerWithExtraOptions = providersWithExtraOptions.find(p => typeof p === 'string' ? p === provider : p.name === provider)
+    if (!providerWithExtraOptions) {
       return Promise.resolve()
     }
 
     this.destinationOptionsLoading = true
-    return ProviderSource.getDestinationOptions(endpointId).then(options => {
+    return ProviderSource.getDestinationOptions(endpointId, envData).then(options => {
       this.optionsSchema.forEach(field => {
         let fieldValues = options.find(f => f.name === field.name)
         if (fieldValues) {
-          // $FlowIgnore
-          field.enum = [...fieldValues.values]
-          if (fieldValues.config_default) {
-            field.default = typeof fieldValues.config_default === 'string' ? fieldValues.config_default : fieldValues.config_default.id
+          if (field.type === 'string') {
+            // $FlowIgnore
+            field.enum = [...fieldValues.values]
+            if (fieldValues.config_default) {
+              field.default = typeof fieldValues.config_default === 'string' ? fieldValues.config_default : fieldValues.config_default.id
+            }
+            // the `migr_image_map` field is special since it needs to group the values by OS type
+          } else if (field.name === 'migr_image_map') {
+            field.properties = [
+              {
+                name: 'windows_image',
+                type: 'string',
+                enum: fieldValues.values.filter(v => typeof v !== 'string' && v.os_type === 'windows'),
+              },
+              {
+                name: 'linux_image',
+                type: 'string',
+                enum: fieldValues.values.filter(v => typeof v !== 'string' && v.os_type === 'linux'),
+              },
+            ]
           }
         }
       })
       this.destinationOptions = options
       this.destinationOptionsLoading = false
-    }).catch(() => { this.destinationOptionsLoading = false })
+    }).catch(() => {
+      if (envData) {
+        return this.loadOptionsSchema(provider, this.lastOptionsSchemaType).then(() => {
+          return this.getDestinationOptions(endpointId, provider)
+        })
+      }
+      return this.loadOptionsSchema(provider, this.lastOptionsSchemaType)
+    }).then(() => {
+      this.destinationOptions = []
+      this.destinationOptionsLoading = false
+    })
   }
 }
 

+ 3 - 2
src/types/Endpoint.js

@@ -34,6 +34,7 @@ export type Endpoint = {
 
 export type DestinationOption = {
   name: string,
-  values: string[] | {name: string, id: string}[],
-  config_default: string | {name: string, id: string},
+  // $FlowIssue
+  values: string[] | { name: string, id: string, [string]: mixed }[],
+  config_default: string | { name: string, id: string },
 }

+ 3 - 1
src/types/Field.js

@@ -19,7 +19,8 @@ export type Field = {
   type?: string,
   value?: any,
   label?: string,
-  enum?: string[],
+  // $FlowIssue
+  enum?: string[] | { id: string, name: string, [string]: mixed }[],
   default?: any,
   items?: Field[],
   fields?: Field[],
@@ -28,4 +29,5 @@ export type Field = {
   parent?: string,
   properties?: Field[],
   required?: boolean,
+  useTextArea?: boolean,
 }

+ 6 - 5
src/utils/ApiCaller.js

@@ -24,14 +24,15 @@ type Cancelable = {
   cancel: () => void,
 }
 
-type RequestOptions = {|
+type RequestOptions = {
   url: string,
   method?: string,
   cancelId?: string,
-  headers?: {[string]: string},
+  headers?: { [string]: string },
   data?: any,
   responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream',
-|}
+  quietError?: boolean,
+}
 
 let cancelables: Cancelable[] = []
 const CancelToken = axios.CancelToken
@@ -71,7 +72,7 @@ class ApiCaller {
       }
 
       if (options.cancelId) {
-        let cancel = () => {}
+        let cancel = () => { }
         axiosOptions.cancelToken = new CancelToken(c => {
           cancel = c
         })
@@ -89,7 +90,7 @@ class ApiCaller {
         if (error.response) {
           // The request was made and the server responded with a status code
           // that falls out of the range of 2xx
-          if (error.response.status !== 401 || window.location.hash !== loginUrl) {
+          if ((error.response.status !== 401 || window.location.hash !== loginUrl) && !options.quietError && error.response.data.error) {
             notificationStore.notify(error.response.data.error.message, 'error')
           }
 

+ 2 - 0
src/utils/LabelDictionary.js

@@ -95,6 +95,8 @@ class LabelDictionary {
     opc: 'Oracle Cloud',
     azure: 'Azure',
     vmware_vsphere: 'VMware',
+    oci: 'OCI',
+    migr_subnet_id: 'Migration Subnet ID',
     separate_vm: 'Separate Migration/VM?',
     use_replica: 'Use replica',
     windows_migr_image: { label: 'Windows Migration Image', description: 'The Windows Migration Image information found on the Azure page' },

+ 4 - 0
src/utils/ObjectUtils.js

@@ -54,6 +54,10 @@ class ObjectUtils {
 
     return result
   }
+
+  static isValid(value: any): boolean {
+    return value !== null && value !== undefined
+  }
 }
 
 export default ObjectUtils

Some files were not shown because too many files changed in this diff