/*
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 .
*/
import React from 'react'
import { observer } from 'mobx-react'
import styled from 'styled-components'
import moment from 'moment'
import StatusPill from '@src/components/ui/StatusComponents/StatusPill'
import { ThemePalette, ThemeProps } from '@src/components/Theme'
import LabelDictionary from '@src/utils/LabelDictionary'
import DateUtils from '@src/utils/DateUtils'
import { migrationFields } from '@src/constants'
import type { Schedule } from '@src/@types/Schedule'
import type { WizardData } from '@src/@types/WizardData'
import type { StorageMap, StorageBackend } from '@src/@types/Endpoint'
import type { Instance, Disk, InstanceScript } from '@src/@types/Instance'
import type { Field } from '@src/@types/Field'
import fieldHelper from '@src/@types/Field'
import { getDisks } from '@src/components/modules/WizardModule/WizardStorage'
import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '@src/components/modules/WizardModule/WizardOptions'
import { MinionPool } from '@src/@types/MinionPool'
import { ProviderTypes } from '@src/@types/Providers'
import configLoader from '@src/utils/Config'
import networkArrowImage from './images/network-arrow.svg'
const Wrapper = styled.div`
width: 100%;
display: flex;
`
const Column = styled.div`
width: 50%;
&:first-child {
margin-right: 160px;
}
&:last-child {
max-width: calc(50% - 160px);
}
`
const Section = styled.div`
margin-bottom: 42px;
&:last-child {
margin-bottom: 0;
}
`
const SectionTitle = styled.div`
font-size: 24px;
font-weight: ${ThemeProps.fontWeights.light};
margin-bottom: 16px;
`
const Overview = styled.div``
const OverviewLabel = styled.div`
font-size: 10px;
font-weight: ${ThemeProps.fontWeights.medium};
text-transform: uppercase;
color: ${ThemePalette.grayscale[5]};
margin-bottom: 4px;
`
const OverviewRow = styled.div`
margin-bottom: 32px;
&:last-child {
margin-bottom: 0;
}
`
const OverviewRowData = styled.div`
display: flex;
`
const OverviewRowLabel = styled.div`
margin-left: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`
const Table = styled.div``
const Row = styled.div`
display: flex;
flex-direction: ${props => props.direction || 'column'};
padding: 8px 0;
border-top: 1px solid ${ThemePalette.grayscale[1]};
color: ${ThemePalette.grayscale[4]};
&:last-child {
border-bottom: 1px solid ${ThemePalette.grayscale[1]};
}
`
const ScriptFileName = styled.div`
max-width: 124px;
text-overflow: ellipsis;
overflow: hidden;
margin-left: 16px;
white-space: nowrap;
flex-shrink: 0;
`
const InstanceRowTitle = styled.div`
margin-bottom: 4px;
`
const InstanceRowSubtitle = styled.div`
font-size: 10px;
color: ${ThemePalette.grayscale[5]};
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
`
const SourceNetwork = styled.div`
width: 50%;
margin-right: 16px;
overflow-wrap: break-word;
`
const NetworkArrow = styled.div`
width: 32px;
height: 16px;
background: url('${networkArrowImage}') center no-repeat;
`
const TargetNetwork = styled.div`
width: 50%;
text-align: right;
margin-left: 20px;
display: flex;
flex-direction: column;
margin-top: -16px;
`
const TargetNetworkName = styled.div`
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
margin-top: 8px;
&:first-child {
margin-top: 16px;
}
`
const OptionsList = styled.div``
const Option = styled.div`
display: flex;
margin-bottom: 8px;
`
const OptionLabel = styled.div`
color: ${ThemePalette.grayscale[4]};
${ThemeProps.exactWidth('50%')}
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const OptionValue = styled.div`
text-align: right;
${ThemeProps.exactWidth('50%')}
text-overflow: ellipsis;
overflow: hidden;
`
const ObjectTable = styled.div`
margin-top: 24px;
`
const ObjectTableTitle = styled.div`
margin-bottom: 8px;
`
type Props = {
data: WizardData,
wizardType: 'replica' | 'migration',
schedules: Schedule[],
minionPools: MinionPool[]
defaultStorage: { value: string | null, busType?: string | null } | undefined,
storageMap: StorageMap[],
instancesDetails: Instance[],
sourceSchema: Field[],
destinationSchema: Field[],
uploadedUserScripts: InstanceScript[],
}
@observer
class WizardSummary extends React.Component {
getDefaultBooleanOption(fieldName: string, defaultValue: boolean): boolean {
if (!this.props.data.destOptions) {
return defaultValue
}
if (this.props.data.destOptions[fieldName] != null) {
return this.props.data.destOptions[fieldName]
}
return defaultValue
}
renderScheduleLabel(schedule: Schedule) {
const scheduleInfo = schedule.schedule
let monthLabel
if (!scheduleInfo) {
return null
}
if (scheduleInfo.month == null) {
monthLabel = 'Every month'
} else {
monthLabel = `Every ${moment.months()[scheduleInfo.month ? scheduleInfo.month - 1 : 0]}`
}
let dayOfMonthLabel
if (scheduleInfo.dom == null) {
dayOfMonthLabel = 'every day'
} else {
dayOfMonthLabel = `every ${DateUtils.getOrdinalDay(scheduleInfo.dom)}`
}
let dayOfWeekLabel
if (scheduleInfo.dow == null) {
dayOfWeekLabel = 'every weekday'
} else {
dayOfWeekLabel = `every ${moment.weekdays(true)[scheduleInfo.dow]}`
}
const padNumber = (number: number) => ((number || 0) < 10 ? `0${number || 0}` : (number || 0).toString())
let timeLabel
if (scheduleInfo.minute == null) {
if (scheduleInfo.hour == null) {
timeLabel = 'every hour, every minute'
} else {
timeLabel = `at ${padNumber(scheduleInfo.hour)} o'clock, every minute UTC`
}
} else if (scheduleInfo.hour == null) {
timeLabel = `every hour, at minute ${padNumber(scheduleInfo.minute)} UTC`
} else {
timeLabel = `at ${padNumber(scheduleInfo.hour)}:${padNumber(scheduleInfo.minute)} UTC`
}
return `${monthLabel}, ${dayOfMonthLabel}, ${dayOfWeekLabel}, ${timeLabel}`
}
renderScheduleSection() {
const schedules = this.props.schedules
if (this.props.wizardType !== 'replica' || !schedules || schedules.length === 0) {
return null
}
return (
Schedule
{schedules.map(schedule => (
{this.renderScheduleLabel(schedule)}
))}
)
}
renderSourceOptionsSection() {
const data = this.props.data
const type = this.props.wizardType.charAt(0).toUpperCase() + this.props.wizardType.substr(1)
const provider = this.props.data && this.props.data.source && this.props.data.source.type
if (!data.sourceOptions) {
return null
}
return (
{type} Source Options
{data.sourceOptions ? Object.keys(data.sourceOptions).map(optionName => {
if (
!data.sourceOptions
|| data.sourceOptions[optionName] == null
|| data.sourceOptions[optionName] === ''
|| typeof data.sourceOptions[optionName] === 'object'
) {
return null
}
const optionLabel = optionName.split('/')
.map(n => LabelDictionary.get(n, `${data.source ? data.source.type : ''}-source`)).join(' - ')
const optionValue = fieldHelper
.getValueAlias(optionName, data.sourceOptions
&& data.sourceOptions[optionName], this.props.sourceSchema, provider)
return (
)
}) : null}
{this.renderObjectTable(data.sourceOptions, this.props.sourceSchema, provider)}
)
}
renderObjectTable(options: any, schema: Field[], provider?: ProviderTypes | null) {
if (!options) {
return null
}
const objectKeys: string[] = Object.keys(options).filter(key => typeof options[key] === 'object'
&& key !== INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS)
return objectKeys.map(key => (options[key] != null ? (
{LabelDictionary.get(key)}
{Object.keys(options[key]).map(propertyName => {
const value = options[key][propertyName]
if (value == null || value === '') {
return null
}
let optionValue
if (key.indexOf('password') > -1 || propertyName.indexOf('password') > -1) {
optionValue = '•••••••••'
} else {
optionValue = fieldHelper.getValueAlias(
propertyName,
value,
schema,
provider,
)
}
return (
)
})}
) : null))
}
renderMinionPoolMapping() {
const allMappings = this.props.data.destOptions?.[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS]
if (!allMappings) {
return null
}
const mappings: any = {}
Object.keys(allMappings).forEach(map => {
if (allMappings[map]) {
mappings[map] = allMappings[map]
}
})
if (!Object.keys(mappings).length) {
return null
}
const getMinionPoolName = (id: string) => {
const minionPool = this.props.minionPools.find(m => m.id === id)
return minionPool?.name || id
}
return (
Instance OSMorphing Minion Pool Mappings
{Object.keys(mappings).map(instanceId => {
const instanceName = this.props.instancesDetails
.find(i => i.instance_name === instanceId || i.id === instanceId)?.name || instanceId
return (
)
})}
)
}
renderTargetOptionsSection() {
const data = this.props.data
const provider = data?.target?.type
const type = this.props.wizardType.charAt(0).toUpperCase() + this.props.wizardType.substr(1)
const executeNowOption = (
)
const separateVmOption = (
)
const migrationOptions = [
(
),
(
),
]
const renderDefaultStorageOption = () => (
)
return (
{type} Target Options
{this.props.wizardType === 'replica' ? executeNowOption : null}
{this.props.wizardType === 'migration' ? migrationOptions : null}
{this.props.data.selectedInstances
&& this.props.data.selectedInstances.length > 1 ? separateVmOption : null}
{this.props.defaultStorage ? renderDefaultStorageOption() : null}
{data.destOptions ? Object.keys(data.destOptions).map(optionName => {
if (
optionName === 'execute_now'
|| optionName === 'separate_vm'
|| migrationFields.find(f => f.name === optionName)
|| !data.destOptions || data.destOptions[optionName] == null || data.destOptions[optionName] === ''
|| typeof data.destOptions[optionName] === 'object'
) {
return null
}
const optionLabel = optionName.split('/')
.map(n => LabelDictionary.get(n, `${data.target ? data.target.type : ''}-destination`)).join(' - ')
const optionValue = fieldHelper.getValueAlias(
optionName,
data.destOptions && data.destOptions[optionName],
this.props.destinationSchema,
provider,
)
return (
)
}) : null}
{this.renderMinionPoolMapping()}
{this.renderObjectTable(data.destOptions, this.props.destinationSchema, provider)}
)
}
renderStorageSection(type: 'backend' | 'disk') {
const storageMap = this.props.storageMap.filter(mapping => mapping.type === type)
const disks = getDisks(this.props.instancesDetails, type)
if (disks.length === 0 || storageMap.length === 0) {
return null
}
const fieldName = type === 'backend' ? 'storage_backend_identifier' : 'id'
let fullStorageMap: { source: Disk, target: StorageBackend | null, busType?: string | null }[] = disks
.filter(d => d[fieldName]).map(disk => {
const diskMapped = storageMap.find(s => s.source[fieldName] === disk[fieldName])
if (diskMapped) {
return { source: diskMapped.source, target: diskMapped.target, busType: diskMapped.targetBusType }
}
return { source: disk, target: null }
})
fullStorageMap.sort((m1, m2) => String(m1.source[fieldName])
.localeCompare(String(m2.source[fieldName])))
fullStorageMap = fullStorageMap.filter(fsm => fsm.target && fsm.target.id)
const title = type === 'backend' ? 'Storage Backend Mapping' : 'Disk Mapping'
if (fullStorageMap.length === 0) {
return null
}
return (
{title}
{fullStorageMap.filter(m => m.target).map(mapping => (
{mapping.source[fieldName]}
{mapping.target ? mapping.target.name : 'Default'}
{mapping.busType ? (
Bus Type: {mapping.busType}
) : null}
))}
)
}
renderNetworksSection() {
const data = this.props.data
if (data.networks == null) {
return null
}
return (
Networks
{data.networks.map(mapping => (
{mapping.sourceNic.network_name}
{mapping.targetNetwork!.name}
{mapping.targetSecurityGroups?.length ? (
Security Groups: {mapping.targetSecurityGroups.map(s => (typeof s === 'string' ? s : s.name)).join(', ')}
) : null}
{mapping.targetPortKey ? (
Port Key: {mapping.targetPortKey}
) : null}
))}
)
}
renderInstancesSection() {
const data = this.props.data
return (
Instances
{data.selectedInstances ? data.selectedInstances.map(instance => {
const flavorName = instance.flavor_name ? `/${instance.flavor_name}` : ''
return (
{instance.name}
{instance.instance_name || instance.id}
{`${instance.num_cpu}vCPU/${instance.memory_mb}MB${flavorName}`}
)
}) : null}
)
}
renderUserScripts() {
if (this.props.uploadedUserScripts.length === 0) {
return null
}
return (
Uploaded User Scripts
{this.props.uploadedUserScripts.map(s => (
{
s.global ? s.global === 'windows' ? 'Global Windows Script' : 'Global Linux Script' : s.instanceId
}
{s.fileName}
))}
)
}
renderOverviewSection() {
const data = this.props.data
const type = this.props.wizardType.charAt(0).toUpperCase() + this.props.wizardType.substr(1)
return (
Overview
Source
{data.source ? data.source.name : ''}
Target
{data.target && data.target.name}
Type
Coriolis {type}
)
}
render() {
return (
{this.renderOverviewSection()}
{this.renderInstancesSection()}
{this.renderNetworksSection()}
{this.renderUserScripts()}
{this.renderSourceOptionsSection()}
{this.renderTargetOptionsSection()}
{this.renderStorageSection('backend')}
{this.renderStorageSection('disk')}
{this.renderScheduleSection()}
)
}
}
export default WizardSummary