/*
Copyright (C) 2025 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 { toJS } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import FieldInput from "@src/components/ui/FieldInput";
import { ThemePalette, ThemeProps } from "@src/components/Theme";
import { deploymentFields } from "@src/constants";
import LabelDictionary from "@src/utils/LabelDictionary";
import type { Field } from "@src/@types/Field";
const Wrapper = styled.div`
display: flex;
min-height: 0;
flex-direction: column;
width: 100%;
`;
const Fields = styled.div`
${props => (props.layout === "page" ? "" : "padding: 32px;")}
display: flex;
flex-direction: column;
overflow: auto;
padding-right: ${props => (props.layout === "page" ? 4 : 24)}px;
flex-grow: 1;
`;
const Group = styled.div`
display: flex;
flex-direction: column;
flex-shrink: 0;
&.field-group-transition-appear {
opacity: 0.01;
}
&.field-group-transition-appear-active {
opacity: 1;
transition: opacity 250ms ease-out;
}
`;
const GroupName = styled.div`
display: flex;
align-items: center;
margin: 48px 0 24px 0;
`;
const GroupNameText = styled.div`
margin: 0 32px;
font-size: 16px;
`;
const GroupNameBar = styled.div`
flex-grow: 1;
background: ${ThemePalette.grayscale[3]};
height: 1px;
`;
const GroupFields = styled.div`
display: flex;
justify-content: space-between;
margin-top: ${props => (props.name === "Deployment options" ? "32px" : "16px")};
`;
const Column = styled.div`
margin-top: -16px;
`;
const FieldInputStyled = styled(FieldInput)`
width: ${props => props.width || ThemeProps.inputSizes.wizard.width}px;
justify-content: space-between;
margin-top: 16px;
`;
export const shouldRenderField = (field: Field) =>
(field.type !== "array" ||
(field.enum && field.enum.length && field.enum.length > 0)) &&
(field.type !== "object" || field.properties);
type FieldRender = {
field: Field;
component: React.ReactNode;
column: number;
};
type Props = {
options: Field[];
wizardType: string;
layout?: "page" | "modal";
data?: { [prop: string]: any } | null;
fieldWidth?: number;
getFieldValue?: (
fieldName: string,
defaultValue: any,
parentFieldName: string | undefined
) => any;
onChange: (field: Field, value: any, parentFieldName?: string) => void;
onScrollableRef?: (ref: HTMLElement) => void;
};
type State = {
executionOptions: { [prop: string]: any } | null;
};
@observer
class WizardExecuteOptions extends React.Component {
state: State = {
executionOptions: null,
};
getDefaultSimpleFieldsSchema() {
const fieldsSchema: Field[] = [];
if (this.props.wizardType === "replica-execute" || this.props.wizardType === "migration-execute") {
fieldsSchema.push({
name: "execute_now",
type: "boolean",
default: true,
nullableBoolean: false,
description:
"When enabled, the transfer will be executed immediately after the options are configured.",
});
fieldsSchema.push({
name: "auto_deploy",
type: "boolean",
default: false,
nullableBoolean: false,
description:
"When enabled, the transfer will automatically deploy the instances on the destination cloud after the transfer is complete.",
});
fieldsSchema.push({
name: "shutdown_instances",
type: "boolean",
default: false,
nullableBoolean: false,
description:
"When enabled, the instances will be shut down before the transfer is executed.",
});
}
return fieldsSchema;
}
generateGroups(fields: FieldRender[]) {
let groups: Array<{ fields: FieldRender[]; name?: string }> = [{ fields }];
if (this.props.wizardType === "replica-execute" || this.props.wizardType === "migration-execute") {
const deploymentFieldNames = deploymentFields.map(f => f.name);
const deploymentFieldsInUse = fields.filter(f =>
deploymentFieldNames.includes(f.field.name)
);
const additionalDeploymentFields = deploymentFields.filter(
f => !fields.some(field => field.field.name === f.name)
).map((field) => ({
column: fields.length % 2,
component: this.renderOptionsField({
...field,
default: field.defaultValue,
}),
field: {
...field,
default: field.defaultValue,
},
}));
if (deploymentFieldsInUse.length > 0 || additionalDeploymentFields.length > 0) {
groups.push({
name: "Deployment options",
fields: [
...deploymentFieldsInUse.map((f, i) => ({ ...f, column: i % 2 })),
...additionalDeploymentFields
],
});
}
}
fields.forEach(f => {
if (f.field.groupName) {
groups[0].fields = groups[0].fields
? groups[0].fields.filter(gf => gf.field.name !== f.field.name)
: [];
const group = groups.find(g => g.name && g.name === f.field.groupName);
if (!group) {
groups.push({
name: f.field.groupName,
fields: [f],
});
} else {
group.fields.push(f);
}
}
});
return groups;
}
getFieldValue(
fieldName: string,
defaultValue: any,
parentFieldName?: string
) {
if (this.props.getFieldValue) {
return this.props.getFieldValue(fieldName, defaultValue, parentFieldName);
}
if (!this.props.data) {
return defaultValue;
}
if (parentFieldName) {
if (
this.props.data[parentFieldName] &&
this.props.data[parentFieldName][fieldName] !== undefined
) {
return this.props.data[parentFieldName][fieldName];
}
return defaultValue;
}
if (!this.props.data || this.props.data[fieldName] === undefined) {
return defaultValue;
}
return this.props.data[fieldName];
}
renderOptionsField(field: Field) {
let additionalProps;
if (field.type === "object" && field.properties) {
additionalProps = {
valueCallback: (f: any) =>
this.getFieldValue(f.name, f.default, field.name),
onChange: (value: any, f: any) => {
this.props.onChange(f, value, field.name);
},
properties: field.properties,
};
} else {
additionalProps = {
value: this.getFieldValue(field.name, field.default, field.groupName),
onChange: (value: any) => {
this.props.onChange(field, value);
},
};
}
return (
);
}
renderOptionsFields() {
let fieldsSchema: Field[] = this.getDefaultSimpleFieldsSchema();
const isRequired = (f: Field) =>
f.required || f.properties?.some(p => p.required);
const defaultFieldNames = fieldsSchema.map(f => f.name);
const filteredOptions = this.props.options.filter(
f => !defaultFieldNames.includes(f.name) && isRequired(f)
);
fieldsSchema = fieldsSchema.concat(filteredOptions);
const nonNullableBooleans: string[] = fieldsSchema
.filter(f => f.type === "boolean" && f.nullableBoolean === false)
.map(f => f.name);
let executeNowColumn: number;
const fields: FieldRender[] = fieldsSchema
.filter(f => shouldRenderField(f))
.map((field, i) => {
let column: number = i % 2;
if (field.name === "execute_now") {
executeNowColumn = column;
}
const usableField = toJS(field);
if (
field.type === "boolean" &&
!nonNullableBooleans.find(name => name === field.name)
) {
usableField.nullableBoolean = true;
}
return {
column,
component: this.renderOptionsField(usableField),
field: usableField,
};
});
const groups = this.generateGroups(fields);
return (
{groups.map((g, i) => {
const getColumnInGroup = (field: any, fieldIndex: number) =>
g.name ? fieldIndex % 2 : field.column;
return (
{g.name ? (
{LabelDictionary.get(g.name)}
) : null}
{g.fields.map(
(f, j) => getColumnInGroup(f, j) === 0 && f.component
)}
{g.fields.map(
(f, j) => getColumnInGroup(f, j) === 1 && f.component
)}
);
})}
);
}
render() {
return (
{this.renderOptionsFields()}
);
}
}
export default WizardExecuteOptions;