/*
Copyright (C) 2020 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 .
*/
import React from 'react'
import styled from 'styled-components'
import { observer } from 'mobx-react'
import { observe } from 'mobx'
import StatusImage from '../../../ui/StatusComponents/StatusImage/StatusImage'
import Button from '../../../ui/Button/Button'
import LoadingButton from '../../../ui/LoadingButton/LoadingButton'
import type { Endpoint as EndpointType } from '../../../../@types/Endpoint'
import { Field, isEnumSeparator } from '../../../../@types/Field'
import ObjectUtils from '../../../../utils/ObjectUtils'
import KeyboardManager from '../../../../utils/KeyboardManager'
import MinionPoolModalContent from './MinionPoolModalContent'
import minionPoolStore from '../../../../stores/MinionPoolStore'
import minionPoolImage from './images/minion-pool.svg'
import notificationStore from '../../../../stores/NotificationStore'
import providerStore, { getFieldChangeOptions } from '../../../../stores/ProviderStore'
import { MinionPool } from '../../../../@types/MinionPool'
import { ThemeProps } from '../../../Theme'
const Wrapper = styled.div`
padding: 24px 0 32px 0;
display: flex;
align-items: center;
flex-direction: column;
min-height: 0;
`
const MinionPoolImageWrapper = styled.div`
${ThemeProps.exactSize('128px')}
background: url('${minionPoolImage}') center no-repeat;
`
const Content = styled.div`
width: 100%;
display: flex;
flex-direction: column;
min-height: 0;
`
const LoadingWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin: 32px 0;
`
const LoadingText = styled.div`
font-size: 18px;
margin-top: 32px;
`
const Buttons = styled.div`
display: flex;
justify-content: space-between;
margin-top: 32px;
flex-shrink: 0;
padding: 0 32px;
`
type Props = {
cancelButtonText: string,
endpoint: EndpointType,
minionPool?: MinionPool | null,
editableData?: any | null
platform: 'source' | 'destination',
onCancelClick: () => void,
onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
onRequestClose: () => void,
onUpdateComplete?: (redirectoTo: string) => void,
}
type State = {
invalidFields: any[],
editableData: any | null
saving: boolean
}
@observer
class MinionPoolModal extends React.Component {
static defaultProps = {
cancelButtonText: 'Cancel',
}
state: State = {
invalidFields: [],
editableData: null,
saving: false,
}
scrollableRef!: HTMLElement
minionPoolStoreObserver!: () => void
UNSAFE_componentWillMount() {
this.UNSAFE_componentWillReceiveProps(this.props)
this.minionPoolStoreObserver = observe(minionPoolStore, () => {
if (this.props.onResizeUpdate) this.props.onResizeUpdate(this.scrollableRef)
})
}
componentDidMount() {
const loadSchema = async () => {
if (!this.props.endpoint) {
return
}
await minionPoolStore.loadMinionPoolSchema(this.props.endpoint.type, this.props.platform)
await providerStore.loadProviders()
const providers = providerStore.providers
if (!providers) {
return
}
await minionPoolStore.loadOptions({
providers,
optionsType: this.props.platform,
endpoint: this.props.endpoint,
envData: this.envData,
useCache: true,
})
this.fillRequiredDefaults()
}
loadSchema()
KeyboardManager.onEnter('minion-pool', () => {
this.create()
}, 2)
}
UNSAFE_componentWillReceiveProps(props: Props) {
if (props.editableData) {
this.setState(prevState => ({
editableData: {
...ObjectUtils.flatten(props.editableData || {}),
...prevState.editableData,
},
}))
}
if (props.platform) {
this.setState(prevState => ({
editableData: {
...prevState.editableData,
platform: props.platform,
},
}))
}
if (props.onResizeUpdate) props.onResizeUpdate(this.scrollableRef)
}
componentWillUnmount() {
KeyboardManager.removeKeyDown('minion-pool')
this.minionPoolStoreObserver()
}
get isLoading() {
return minionPoolStore.loadingMinionPoolSchema
|| minionPoolStore.loadingMinionPools
|| minionPoolStore.optionsPrimaryLoading
|| providerStore.providersLoading
}
get envData() {
let envData: any = null
Object.keys(this.state.editableData).forEach(prop => {
if (!minionPoolStore.minionPoolDefaultSchema.find(f => f.name === prop)) {
envData = envData || {}
envData[prop] = this.state.editableData[prop]
}
})
return envData
}
getFieldValue(field?: Field | null) {
if (!field || !this.state.editableData) {
return ''
}
if (this.state.editableData[field.name] != null) {
return this.state.editableData[field.name]
}
if (Object.keys(field).find(k => k === 'default')) {
return field.default
}
if (field.type === 'integer' || field.type === 'boolean') {
return null
}
return ''
}
findInvalidFields = () => {
const invalidFields = minionPoolStore.minionPoolCombinedSchema.filter(field => {
if (field.required) {
const value = this.getFieldValue(field)
if (value === null || value === '' || value.length === 0) {
return true
}
if (!field.enum) {
return false
}
// When loading new options as a result of destination options calls,
// the value stored in the state may no longer be found in the field's enum.
// Example: When changing the AD of an OCI minion pool,
// although the Subnet ID may show 'Choose Value', the modal would still let you hit 'Update'.
if (!field.enum.find(f => (!isEnumSeparator(f) ? (typeof f === 'string' ? f === value : (f.value === value || f.id === value)) : false))) {
return true
}
}
return false
}).map(f => f.name)
return invalidFields
}
highlightRequired() {
const invalidFields = this.findInvalidFields()
this.setState({ invalidFields })
if (invalidFields.length > 0) {
notificationStore.alert('Please fill the required fields', 'error')
return true
}
return false
}
async create() {
if (this.highlightRequired()) {
return
}
this.setState({ saving: true })
try {
if (this.props.minionPool?.id) {
await this.update()
} else {
await this.add()
}
} catch (err) {
console.error(err)
this.setState({ saving: false })
}
}
async update() {
const stateMinionPool = {
...this.state.editableData,
id: this.props.minionPool?.id,
}
delete stateMinionPool.platform
delete stateMinionPool.endpoint_id
await minionPoolStore.update(this.props.endpoint.type, stateMinionPool)
if (this.props.onUpdateComplete) {
this.props.onUpdateComplete(`/minion-pools/${this.props.minionPool?.id}`)
}
}
async add() {
await minionPoolStore.add(this.props.endpoint.type, this.props.endpoint.id, this.state.editableData)
notificationStore.alert('Minion Pool created', 'success')
this.props.onRequestClose()
}
fillRequiredDefaults() {
this.setState(prevState => {
const minionPool: any = { ...prevState.editableData }
const requiredFieldsDefaults = minionPoolStore.minionPoolCombinedSchema
.filter(f => f.required && f.default != null)
requiredFieldsDefaults.forEach(f => {
if (minionPool[f.name] == null) {
minionPool[f.name] = f.default
}
})
return { editableData: minionPool }
})
}
async loadExtraOptions(field: Field | null, type: 'source' | 'destination', useCache: boolean = true) {
const envData = getFieldChangeOptions({
providerName: this.props.endpoint.type,
schema: minionPoolStore.minionPoolEnvSchema,
data: this.envData,
field,
type,
})
if (!envData) {
return
}
await minionPoolStore.loadOptions({
providers: providerStore.providers!,
optionsType: type,
endpoint: this.props.endpoint,
envData,
useCache,
})
this.fillRequiredDefaults()
}
handleFieldChange(field: Field, value: any) {
this.setState(prevState => {
const minionPool: any = { ...prevState.editableData }
if (field.type === 'array') {
const arrayItems = minionPool[field.name] || []
value = arrayItems.find((v: any) => v === value)
? arrayItems.filter((v: any) => v !== value) : [...arrayItems, value]
}
minionPool[field.name] = value
return { editableData: minionPool }
}, () => {
if (field.type !== 'string' || field.enum) {
this.loadExtraOptions(field, this.props.platform, true)
}
})
}
handleCancelClick() {
this.props.onCancelClick()
}
renderButtons() {
let actionButton = (
)
if (this.state.saving) {
actionButton = Saving ...
}
return (
{actionButton}
)
}
renderContent() {
return (
f.name)}
envOptionsDisabled={this.props.minionPool != null && this.props.minionPool.status !== 'DEALLOCATED'}
defaultSchema={minionPoolStore.minionPoolDefaultSchema}
envSchema={minionPoolStore.minionPoolEnvSchema}
invalidFields={this.state.invalidFields}
disabled={this.state.saving}
cancelButtonText={this.props.cancelButtonText}
getFieldValue={field => this.getFieldValue(field)}
onFieldChange={(field, value) => {
if (field) {
this.handleFieldChange(field, value)
}
}}
onCreateClick={() => { this.create() }}
onCancelClick={() => { this.handleCancelClick() }}
scrollableRef={ref => { this.scrollableRef = ref }}
onResizeUpdate={() => {
if (this.props.onResizeUpdate) {
this.props.onResizeUpdate(this.scrollableRef)
}
}}
/>
{this.renderButtons()}
)
}
renderLoading() {
return (
Loading Pool Options ...
)
}
render() {
return (
{!this.isLoading ? this.renderContent() : null}
{this.isLoading ? this.renderLoading() : null}
)
}
}
export default MinionPoolModal