فهرست منبع

Merge pull request #148 from smiclea/CORWEB-168

Implement Openstack Plugin CORWEB 168
Dorin Paslaru 8 سال پیش
والد
کامیت
f99a4c959f

+ 2 - 1
src/components/atoms/ToggleButtonBar/index.jsx

@@ -55,6 +55,7 @@ type Props = {
   items: Array<ItemType>,
   selectedValue: string,
   onChange: (item: ItemType) => void,
+  className?: string,
 }
 class ToggleButtonBar extends React.Component<Props> {
   render() {
@@ -63,7 +64,7 @@ class ToggleButtonBar extends React.Component<Props> {
     }
 
     return (
-      <Wrapper>
+      <Wrapper className={this.props.className}>
         {this.props.items.map(item => {
           return (
             <Item

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

@@ -374,7 +374,7 @@ class Endpoint extends React.Component<Props, State> {
           handleCancelClick: () => { this.handleCancelClick() },
           scrollableRef: ref => { this.scrollableRef = ref },
           onRef: ref => { this.contentPluginRef = ref },
-          onResizeUpdate: (scrollableRef, scrollOffset) => { this.props.onResizeUpdate(this.scrollableRef, scrollOffset) },
+          onResizeUpdate: (scrollOffset: number) => { this.props.onResizeUpdate(this.scrollableRef, scrollOffset) },
         })}
         {this.renderButtons()}
         <Tooltip />

+ 2 - 3
src/plugins/endpoint/azure/ContentPlugin.jsx

@@ -74,7 +74,7 @@ type Props = {
   cancelButtonText: string,
   validating: boolean,
   onRef: (contentPlugin: any) => void,
-  onResizeUpdate: (fieldsRef: HTMLElement, scrollOfset: number) => void,
+  onResizeUpdate: (scrollOfset: number) => void,
   scrollableRef: (ref: HTMLElement) => void,
 }
 type State = {
@@ -83,7 +83,6 @@ type State = {
 }
 class ContentPlugin extends React.Component<Props, State> {
   cloudProfileChanged: boolean
-  fieldsRef: HTMLElement
   lastBlurValue: string
 
   constructor() {
@@ -105,7 +104,7 @@ class ContentPlugin extends React.Component<Props, State> {
       if (prevState.showPasteInput !== this.state.showPasteInput && this.state.showPasteInput) {
         scrollOffset = 100
       }
-      this.props.onResizeUpdate(this.fieldsRef, scrollOffset)
+      this.props.onResizeUpdate(scrollOffset)
       this.cloudProfileChanged = false
     }
   }

+ 12 - 6
src/plugins/endpoint/default/SchemaPlugin.js

@@ -12,7 +12,12 @@ 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 => {
+// @flow
+
+import type { Schema } from '../../../types/Schema'
+import type { Field } from '../../../types/Field'
+
+export const defaultSchemaToFields = (schema: Schema) => {
   let fields = Object.keys(schema.properties).map(fieldName => {
     let field = {
       ...schema.properties[fieldName],
@@ -25,7 +30,7 @@ export const defaultSchemaToFields = schema => {
   return fields
 }
 
-export const connectionSchemaToFields = schema => {
+export const connectionSchemaToFields = (schema: Schema) => {
   let fields = defaultSchemaToFields(schema)
 
   let sortPriority = { username: 1, password: 2 }
@@ -45,12 +50,13 @@ export const connectionSchemaToFields = schema => {
   return fields
 }
 
-export const generateField = (name, label, required = false, type = 'string', defaultValue = null) => {
+export const generateField = (name: string, label: string, required: boolean = false, type: string = 'string', defaultValue: any = null) => {
   let field = {
     name,
     label,
     type,
     required,
+    default: undefined,
   }
 
   if (defaultValue) {
@@ -60,7 +66,7 @@ export const generateField = (name, label, required = false, type = 'string', de
   return field
 }
 
-export const fieldsToPayload = (data, schema) => {
+export const fieldsToPayload = (data: { [string]: mixed }, schema: Schema) => {
   let info = {}
 
   Object.keys(schema.properties).forEach(fieldName => {
@@ -73,7 +79,7 @@ export const fieldsToPayload = (data, schema) => {
 }
 
 export default class ConnectionSchemaParser {
-  static parseSchemaToFields(schema) {
+  static parseSchemaToFields(schema: Schema): Field[] {
     let fields = connectionSchemaToFields(schema.oneOf[0])
 
     fields = [
@@ -85,7 +91,7 @@ export default class ConnectionSchemaParser {
     return fields
   }
 
-  static parseFieldsToPayload(data, schema) {
+  static parseFieldsToPayload(data: { [string]: mixed }, schema: Schema) {
     let payload = {}
 
     payload.name = data.name

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

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

+ 170 - 0
src/plugins/endpoint/openstack/ContentPlugin.jsx

@@ -0,0 +1,170 @@
+/*
+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 React from 'react'
+import styled from 'styled-components'
+
+import ToggleButtonBar from '../../../components/atoms/ToggleButtonBar'
+import type { Field } from '../../../types/Field'
+import { Wrapper, Fields, FieldStyled, Row } from '../default/ContentPlugin'
+
+const ToggleButtonBarStyled = styled(ToggleButtonBar)`
+  margin-top: 16px;
+`
+
+type Props = {
+  connectionInfoSchema: Field[],
+  validation: { valid: boolean, validation: { message: string } },
+  invalidFields: string[],
+  getFieldValue: (field: ?Field) => any,
+  handleFieldChange: (field: Field, value: any) => void,
+  disabled: boolean,
+  cancelButtonText: string,
+  validating: boolean,
+  onRef: (contentPlugin: any) => void,
+  onResizeUpdate: (scrollOfset: number) => void,
+  scrollableRef: (ref: HTMLElement) => void,
+}
+type State = {
+  useAdvancedOptions: boolean,
+}
+class ContentPlugin extends React.Component<Props, State> {
+  constructor() {
+    super()
+    this.state = {
+      useAdvancedOptions: false,
+    }
+  }
+
+  componentDidMount() {
+    this.props.onRef(this)
+  }
+
+  componentDidUpdate(prevProps: Props, prevState: State) {
+    if (prevState.useAdvancedOptions !== this.state.useAdvancedOptions) {
+      this.props.onResizeUpdate(0)
+    }
+  }
+
+  componentWillUnmount() {
+    this.props.onRef(undefined)
+  }
+
+  handleAdvancedOptionsToggle(useAdvancedOptions: boolean) {
+    this.setState({ useAdvancedOptions })
+  }
+
+  findInvalidFields = () => {
+    const apiVersion = this.props.getFieldValue(this.props.connectionInfoSchema.find(n => n.name === 'identity_api_version'))
+    const invalidFields = this.props.connectionInfoSchema.filter(field => {
+      let required
+      if (typeof field.required === 'function') {
+        required = field.required(apiVersion)
+      } else {
+        required = field.required
+      }
+      if (required) {
+        let value = this.props.getFieldValue(field)
+        return !value
+      }
+      return false
+    }).map(f => f.name)
+
+    return invalidFields
+  }
+
+  filterSimpleAdvanced(): Field[] {
+    const apiVersion = this.props.getFieldValue(this.props.connectionInfoSchema.find(n => n.name === 'identity_api_version'))
+    const extraAdvancedFields = ['description', 'glance_api_version', 'identity_api_version']
+    return this.props.connectionInfoSchema.filter(field => {
+      if (this.state.useAdvancedOptions) {
+        return true
+      }
+      let required
+      if (typeof field.required === 'function') {
+        required = field.required(apiVersion)
+      } else {
+        required = field.required
+      }
+      return required || extraAdvancedFields.find(fieldName => field.name === fieldName)
+    })
+  }
+
+  renderSimpleAdvancedToggle() {
+    return (
+      <ToggleButtonBarStyled
+        items={[{ label: 'Simple', value: 'simple' }, { label: 'Advanced', value: 'advanced' }]}
+        selectedValue={this.state.useAdvancedOptions ? 'advanced' : 'simple'}
+        onChange={item => { this.handleAdvancedOptionsToggle(item.value === 'advanced') }}
+      />
+    )
+  }
+
+  renderFields() {
+    const rows = []
+    let lastField
+    let apiVersion = this.props.getFieldValue(this.props.connectionInfoSchema.find(n => n.name === 'identity_api_version'))
+
+    let fields = this.filterSimpleAdvanced()
+
+    fields.forEach((field, i) => {
+      const currentField = (
+        <FieldStyled
+          {...field}
+          required={typeof field.required === 'function' ? field.required(apiVersion) : field.required}
+          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 innerRef={ref => { this.props.scrollableRef(ref) }}>
+        {rows}
+      </Fields>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {this.renderSimpleAdvancedToggle()}
+        {this.renderFields()}
+      </Wrapper>
+    )
+  }
+}
+
+export default ContentPlugin

+ 75 - 0
src/plugins/endpoint/openstack/SchemaPlugin.js

@@ -0,0 +1,75 @@
+/*
+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'
+
+const customSort = (fields: Field[]) => {
+  const sortPriority = {
+    name: 1,
+    description: 2,
+    username: 3,
+    password: 4,
+    auth_url: 5,
+    project_name: 6,
+    glance_api_version: 7,
+    identity_api_version: 8,
+    project_domain_name: 9,
+    user_domain_name: 10,
+  }
+  fields.sort((a, b) => {
+    if (sortPriority[a.name] && sortPriority[b.name]) {
+      return sortPriority[a.name] - sortPriority[b.name]
+    }
+    if (sortPriority[a.name]) {
+      return -1
+    }
+    if (sortPriority[b.name]) {
+      return 1
+    }
+    return a.name.localeCompare(b.name)
+  })
+
+  return fields
+}
+
+export default class ConnectionSchemaParser {
+  static parseSchemaToFields(schema: Schema): Field[] {
+    let fields = DefaultConnectionSchemaParser.parseSchemaToFields(schema)
+    let identityField = fields.find(f => f.name === 'identity_api_version')
+    if (identityField && !identityField.default) {
+      identityField.default = identityField.minimum
+    }
+    customSort(fields)
+
+    let projectDomainField = fields.find(f => f.name === 'project_domain_name')
+    let userDomainField = fields.find(f => f.name === 'user_domain_name')
+    let requiredFunc = (apiVersion: number) => apiVersion > 2
+    if (projectDomainField && userDomainField) {
+      projectDomainField.required = requiredFunc
+      userDomainField.required = requiredFunc
+    }
+
+    return fields
+  }
+
+  static parseFieldsToPayload(data: { [string]: mixed }, schema: Schema) {
+    let payload = DefaultConnectionSchemaParser.parseFieldsToPayload(data, schema)
+    return payload
+  }
+}

+ 3 - 1
src/types/Field.js

@@ -19,8 +19,10 @@ export type Field = {
   type?: string,
   value?: any,
   enum?: string[],
-  required?: boolean,
+  required?: boolean | (value: any) => boolean,
   default?: any,
   items?: Field[],
   fields?: Field[],
+  minimum?: number,
+  maximum?: number,
 }

+ 25 - 0
src/types/Schema.js

@@ -0,0 +1,25 @@
+/*
+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
+
+export type Schema = {
+  properties: {
+    [string]: {
+      name: string,
+    }
+  },
+  required: string[],
+  oneOf: Schema[],
+}