/*
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 { Info } from "luxon";
import { observer } from "mobx-react";
import React from "react";
import styled from "styled-components";
import fieldHelper from "@src/@types/Field";
import { MinionPool } from "@src/@types/MinionPool";
import { ProviderTypes } from "@src/@types/Providers";
import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from "@src/components/modules/WizardModule/WizardOptions";
import { getDisks } from "@src/components/modules/WizardModule/WizardStorage";
import { ThemePalette, ThemeProps } from "@src/components/Theme";
import StatusPill from "@src/components/ui/StatusComponents/StatusPill";
import configLoader from "@src/utils/Config";
import DateUtils from "@src/utils/DateUtils";
import LabelDictionary from "@src/utils/LabelDictionary";
import networkArrowImage from "./images/network-arrow.svg";
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";
const Wrapper = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
`;
const Column = styled.div`
${ThemeProps.exactWidth("calc(50% - 80px)")}
`;
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 ${
Info.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 ${Info.weekdays()[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({
name: optionName,
value: data.sourceOptions?.[optionName],
fields: this.props.sourceSchema,
targetProvider: 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({
name: propertyName,
value,
fields: schema,
targetProvider: 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 renderDefaultStorageOption = () => (
);
return (
{type} Target Options
{this.props.wizardType === "replica" ? executeNowOption : null}
{this.props.wizardType === "migration" ? executeNowOption : 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" ||
!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({
name: optionName,
value: data.destOptions?.[optionName],
fields: this.props.destinationSchema,
targetProvider: 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;