/*
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 .
*/
// @flow
import * as React from 'react'
import styled from 'styled-components'
import configLoader from '../../../utils/Config'
import ToggleButtonBar from '../../../components/atoms/ToggleButtonBar'
import type { Field } from '../../../types/Field'
import { Wrapper, FieldStyled, Row } from '../default/ContentPlugin'
import StyleProps from '../../../components/styleUtils/StyleProps'
import Palette from '../../../components/styleUtils/Palette'
const ToggleButtonBarStyled = styled(ToggleButtonBar)`
margin-top: 16px;
`
const Fields = styled.div`
margin-top: 32px;
padding: 0 32px;
display: flex;
flex-direction: column;
overflow: auto;
`
const Group = styled.div`
display: flex;
flex-direction: column;
flex-shrink: 0;
`
const GroupName = styled.div`
display: flex;
align-items: center;
margin: 32px 0 24px 0;
`
const GroupNameText = styled.div`
margin: 0 32px;
font-size: 16px;
`
const GroupNameBar = styled.div`
flex-grow: 1;
background: ${Palette.grayscale[3]};
height: 1px;
`
const GroupFields = styled.div`
display: flex;
justify-content: space-between;
flex-direction: column;
`
type Props = {
connectionInfoSchema: Field[],
validation: { valid: boolean, validation: { message: string } },
invalidFields: string[],
getFieldValue: (field: ?Field) => any,
handleFieldChange: (field: ?Field, value: any) => void,
handleFieldsChange: (items: { 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,
showCephOptions: boolean,
}
class ContentPlugin extends React.Component {
// This is a temporary hack, should be always true for all plugins, but momentaraly causes issues in Azure plugins
// Fix Azure plugin and remove this line
static REQUIRES_PARENT_OBJECT_PATH = true
state = {
useAdvancedOptions: false,
showCephOptions: false,
}
previouslySelectedChoices: string[] = []
get useCurrentUser(): boolean {
return Boolean(this.getFieldValue(this.props.connectionInfoSchema.find(n => n.name === 'openstack_use_current_user')))
}
get hasCephOptionsSet(): boolean {
console.log('schema', JSON.parse(JSON.stringify(this.props.connectionInfoSchema)))
let cephOptionsField = this.props.connectionInfoSchema.find(n => n.name === 'ceph_options')
if (!cephOptionsField || !cephOptionsField.properties) {
return false
}
let hasValues = cephOptionsField.properties.filter(f => this.getFieldValue(f))
return hasValues.length > 0
}
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)
}
getApiVersion(): number {
return this.props.getFieldValue(this.props.connectionInfoSchema.find(n => n.name === 'identity_api_version'))
}
getFieldValue(field: ?Field) {
let fieldValue = this.props.getFieldValue(field)
if (fieldValue) {
return fieldValue
}
let getInputChoiceValue = (fieldBaseName: string): string => {
let id = this.props.getFieldValue(this.props.connectionInfoSchema.find(n => n.name === `${fieldBaseName}_id`))
let previouslySelected = this.previouslySelectedChoices.find(f => f === `${fieldBaseName}_id`)
if (id || previouslySelected) {
if (!previouslySelected) this.previouslySelectedChoices.push(`${fieldBaseName}_id`)
return `${fieldBaseName}_id`
}
return `${fieldBaseName}_name`
}
if (field && field.name === 'user_domain') {
return getInputChoiceValue('user_domain')
}
if (field && field.name === 'project_domain') {
return getInputChoiceValue('project_domain')
}
return fieldValue
}
handleAdvancedOptionsToggle(useAdvancedOptions: boolean) {
this.setState({ useAdvancedOptions })
}
handleShowCepthOptionsChange(value: boolean) {
let cephOptions = this.props.connectionInfoSchema.find(f => f.name === 'ceph_options')
if (!cephOptions || !cephOptions.properties) {
return
}
let resetFields = cephOptions.properties.map(field => ({
field,
value: null,
}))
this.props.handleFieldsChange(resetFields)
this.setState({ showCephOptions: value })
}
findInvalidFields = () => {
let inputChoices = ['user_domain', 'project_domain']
let invalidFields = this.props.connectionInfoSchema.filter(field => {
if (this.isFieldRequired(field)) {
let value = this.getFieldValue(field)
return !value
}
let inputChoice = inputChoices.find(c => c === field.name)
if (inputChoice && this.getApiVersion() > 2) {
let selectionValue = this.getFieldValue(this.props.connectionInfoSchema.find(f => f.name === inputChoice))
let itemValue = this.getFieldValue(this.props.connectionInfoSchema.find(f => f.name === selectionValue))
return !itemValue
}
return false
}).map(f => f.name)
let cephOptions = this.props.connectionInfoSchema.find(f => f.name === 'ceph_options')
let cephOptionsProperties = cephOptions && cephOptions.properties
if (cephOptionsProperties && (this.state.showCephOptions || this.hasCephOptionsSet)) {
invalidFields = invalidFields.concat(
cephOptionsProperties.filter(f => f.required && !this.getFieldValue(f)).map(f => f.name)
)
}
return invalidFields
}
filterSimpleAdvanced(): Field[] {
let extraAdvancedFields = ['description', 'glance_api_version', 'identity_api_version', 'openstack_use_current_user']
if (this.getApiVersion() > 2) {
extraAdvancedFields = extraAdvancedFields.concat(['user_domain', 'project_domain'])
}
let ignoreFields = ['user_domain_id', 'project_domain_id', 'user_domain_name', 'project_domain_name']
if (!configLoader.config.showOpenstackCurrentUserSwitch) {
ignoreFields.push('openstack_use_current_user')
}
return this.props.connectionInfoSchema.filter(f => !ignoreFields.find(i => i === f.name)).filter(field => {
if (field.name === 'ceph_options') {
return this.state.useAdvancedOptions && (this.state.showCephOptions || this.hasCephOptionsSet)
}
if (this.state.useAdvancedOptions) {
return true
}
return field.required || extraAdvancedFields.find(fieldName => field.name === fieldName)
})
}
isFieldRequired(field: Field) {
return this.useCurrentUser ? field.name === 'name' : field.required
}
renderSimpleAdvancedToggle() {
return (
{ this.handleAdvancedOptionsToggle(item.value === 'advanced') }}
/>
)
}
renderFields() {
const rows = []
let fields = this.filterSimpleAdvanced()
if (this.state.useAdvancedOptions) {
let showCepthOptionsField = {
name: 'show_ceph_options',
label: 'Use Ceph for Replication',
type: 'boolean',
description: 'If performing Ceph-based Replicas from a source OpenStack, the Ceph configuration file and credentials for a user with read-only access to the Ceph pool used by Cinder backups/snapshots must be provided. Coriolis must be able to connect to the source OpenStack\'s Ceph RADOS cluster by being able to reach at least one Ceph- monitor host.For the easiest setup possible, simply using the same credentials used by the Cinder service(s) will work.',
}
fields.push(showCepthOptionsField)
}
const renderField = field => {
let disabled = this.props.disabled
|| (this.useCurrentUser && field.name !== 'name' && field.name !== 'description' && field.name !== 'openstack_use_current_user')
let required = this.isFieldRequired(field)
|| (this.getApiVersion() > 2 ? field.name === 'user_domain' || field.name === 'project_domain' : false)
let isPassword = Boolean(configLoader.config.passwordFields.find(fn => field.name === fn))
|| field.name.indexOf('password') > -1
let value = field.name === 'show_ceph_options' ? (this.state.showCephOptions || this.hasCephOptionsSet) : this.getFieldValue(field)
let onChange = value => {
if (field.name === 'show_ceph_options') {
this.handleShowCepthOptionsChange(value)
} else {
this.props.handleFieldChange(field, value)
}
}
return (
fn === field.name) > -1}
value={value}
onChange={onChange}
getFieldValue={fieldName => this.getFieldValue(this.props.connectionInfoSchema.find(n => n.name === fieldName))}
onFieldChange={(fieldName, fieldValue) => { this.props.handleFieldChange(this.props.connectionInfoSchema.find(n => n.name === fieldName), fieldValue) }}
/>
)
}
let lastField = null
let nonCephFields = fields.filter(f => f.name !== 'ceph_options')
nonCephFields.forEach((field, i) => {
const currentField = renderField(field)
if (i % 2 !== 0) {
rows.push((
{lastField}
{currentField}
))
} else if (i === nonCephFields.length - 1) {
rows.push((
{currentField}
))
}
lastField = currentField
})
const cephOptionsRows = []
let cephOptionsField = fields.find(f => f.name === 'ceph_options')
let cephOptions = null
let properties = cephOptionsField && cephOptionsField.properties
if (properties) {
let i = 0
properties.forEach((field, fieldIndex) => {
if (field.name === 'ceph_options/ceph_conf_file' || field.name === 'ceph_options/ceph_keyring_file') {
field.useTextArea = true
}
const currentField = renderField(field)
const pushRow = (field1: React.Node, field2?: React.Node) => {
cephOptionsRows.push((
{field1}
{field2}
))
}
if (field.useTextArea) {
pushRow(currentField)
i -= 1
} else if (i % 2 !== 0) {
pushRow(lastField, currentField)
} else if (fieldIndex === properties.length - 1) {
pushRow(currentField)
if (field.useTextArea) {
i -= 1
}
} else {
lastField = currentField
}
i += 1
})
cephOptions = (
Ceph Options
{cephOptionsRows}
)
}
return (
{ this.props.scrollableRef(ref) }}>
{rows}
{cephOptions}
)
}
render() {
return (
{this.renderSimpleAdvancedToggle()}
{this.renderFields()}
)
}
}
export default ContentPlugin