/* 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