WizardExecuteOptions.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /*
  2. Copyright (C) 2025 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import { toJS } from "mobx";
  15. import { observer } from "mobx-react";
  16. import * as React from "react";
  17. import styled from "styled-components";
  18. import FieldInput from "@src/components/ui/FieldInput";
  19. import { ThemePalette, ThemeProps } from "@src/components/Theme";
  20. import { deploymentFields } from "@src/constants";
  21. import LabelDictionary from "@src/utils/LabelDictionary";
  22. import type { Field } from "@src/@types/Field";
  23. const Wrapper = styled.div<any>`
  24. display: flex;
  25. min-height: 0;
  26. flex-direction: column;
  27. width: 100%;
  28. `;
  29. const Fields = styled.div<any>`
  30. ${props => (props.layout === "page" ? "" : "padding: 32px;")}
  31. display: flex;
  32. flex-direction: column;
  33. overflow: auto;
  34. padding-right: ${props => (props.layout === "page" ? 4 : 24)}px;
  35. flex-grow: 1;
  36. `;
  37. const Group = styled.div<any>`
  38. display: flex;
  39. flex-direction: column;
  40. flex-shrink: 0;
  41. &.field-group-transition-appear {
  42. opacity: 0.01;
  43. }
  44. &.field-group-transition-appear-active {
  45. opacity: 1;
  46. transition: opacity 250ms ease-out;
  47. }
  48. `;
  49. const GroupName = styled.div<any>`
  50. display: flex;
  51. align-items: center;
  52. margin: 48px 0 24px 0;
  53. `;
  54. const GroupNameText = styled.div<any>`
  55. margin: 0 32px;
  56. font-size: 16px;
  57. `;
  58. const GroupNameBar = styled.div<any>`
  59. flex-grow: 1;
  60. background: ${ThemePalette.grayscale[3]};
  61. height: 1px;
  62. `;
  63. const GroupFields = styled.div<any>`
  64. display: flex;
  65. justify-content: space-between;
  66. margin-top: ${props => (props.name === "Deployment options" ? "32px" : "16px")};
  67. `;
  68. const Column = styled.div<any>`
  69. margin-top: -16px;
  70. `;
  71. const FieldInputStyled = styled(FieldInput)`
  72. width: ${props => props.width || ThemeProps.inputSizes.wizard.width}px;
  73. justify-content: space-between;
  74. margin-top: 16px;
  75. `;
  76. export const shouldRenderField = (field: Field) =>
  77. (field.type !== "array" ||
  78. (field.enum && field.enum.length && field.enum.length > 0)) &&
  79. (field.type !== "object" || field.properties);
  80. type FieldRender = {
  81. field: Field;
  82. component: React.ReactNode;
  83. column: number;
  84. };
  85. type Props = {
  86. options: Field[];
  87. wizardType: string;
  88. layout?: "page" | "modal";
  89. data?: { [prop: string]: any } | null;
  90. fieldWidth?: number;
  91. getFieldValue?: (
  92. fieldName: string,
  93. defaultValue: any,
  94. parentFieldName: string | undefined
  95. ) => any;
  96. onChange: (field: Field, value: any, parentFieldName?: string) => void;
  97. onScrollableRef?: (ref: HTMLElement) => void;
  98. };
  99. type State = {
  100. executionOptions: { [prop: string]: any } | null;
  101. };
  102. @observer
  103. class WizardExecuteOptions extends React.Component<Props, State> {
  104. state: State = {
  105. executionOptions: null,
  106. };
  107. getDefaultSimpleFieldsSchema() {
  108. const fieldsSchema: Field[] = [];
  109. if (this.props.wizardType === "replica-execute" || this.props.wizardType === "migration-execute") {
  110. fieldsSchema.push({
  111. name: "execute_now",
  112. type: "boolean",
  113. default: true,
  114. nullableBoolean: false,
  115. description:
  116. "When enabled, the transfer will be executed immediately after the options are configured.",
  117. });
  118. fieldsSchema.push({
  119. name: "auto_deploy",
  120. type: "boolean",
  121. default: false,
  122. nullableBoolean: false,
  123. description:
  124. "When enabled, the transfer will automatically deploy the instances on the destination cloud after the transfer is complete.",
  125. });
  126. fieldsSchema.push({
  127. name: "shutdown_instances",
  128. type: "boolean",
  129. default: false,
  130. nullableBoolean: false,
  131. description:
  132. "When enabled, the instances will be shut down before the transfer is executed.",
  133. });
  134. }
  135. return fieldsSchema;
  136. }
  137. generateGroups(fields: FieldRender[]) {
  138. let groups: Array<{ fields: FieldRender[]; name?: string }> = [{ fields }];
  139. if (this.props.wizardType === "replica-execute" || this.props.wizardType === "migration-execute") {
  140. const deploymentFieldNames = deploymentFields.map(f => f.name);
  141. const deploymentFieldsInUse = fields.filter(f =>
  142. deploymentFieldNames.includes(f.field.name)
  143. );
  144. const additionalDeploymentFields = deploymentFields.filter(
  145. f => !fields.some(field => field.field.name === f.name)
  146. ).map((field) => ({
  147. column: fields.length % 2,
  148. component: this.renderOptionsField({
  149. ...field,
  150. default: field.defaultValue,
  151. }),
  152. field: {
  153. ...field,
  154. default: field.defaultValue,
  155. },
  156. }));
  157. if (deploymentFieldsInUse.length > 0 || additionalDeploymentFields.length > 0) {
  158. groups.push({
  159. name: "Deployment options",
  160. fields: [
  161. ...deploymentFieldsInUse.map((f, i) => ({ ...f, column: i % 2 })),
  162. ...additionalDeploymentFields
  163. ],
  164. });
  165. }
  166. }
  167. fields.forEach(f => {
  168. if (f.field.groupName) {
  169. groups[0].fields = groups[0].fields
  170. ? groups[0].fields.filter(gf => gf.field.name !== f.field.name)
  171. : [];
  172. const group = groups.find(g => g.name && g.name === f.field.groupName);
  173. if (!group) {
  174. groups.push({
  175. name: f.field.groupName,
  176. fields: [f],
  177. });
  178. } else {
  179. group.fields.push(f);
  180. }
  181. }
  182. });
  183. return groups;
  184. }
  185. getFieldValue(
  186. fieldName: string,
  187. defaultValue: any,
  188. parentFieldName?: string
  189. ) {
  190. if (this.props.getFieldValue) {
  191. return this.props.getFieldValue(fieldName, defaultValue, parentFieldName);
  192. }
  193. if (!this.props.data) {
  194. return defaultValue;
  195. }
  196. if (parentFieldName) {
  197. if (
  198. this.props.data[parentFieldName] &&
  199. this.props.data[parentFieldName][fieldName] !== undefined
  200. ) {
  201. return this.props.data[parentFieldName][fieldName];
  202. }
  203. return defaultValue;
  204. }
  205. if (!this.props.data || this.props.data[fieldName] === undefined) {
  206. return defaultValue;
  207. }
  208. return this.props.data[fieldName];
  209. }
  210. renderOptionsField(field: Field) {
  211. let additionalProps;
  212. if (field.type === "object" && field.properties) {
  213. additionalProps = {
  214. valueCallback: (f: any) =>
  215. this.getFieldValue(f.name, f.default, field.name),
  216. onChange: (value: any, f: any) => {
  217. this.props.onChange(f, value, field.name);
  218. },
  219. properties: field.properties,
  220. };
  221. } else {
  222. additionalProps = {
  223. value: this.getFieldValue(field.name, field.default, field.groupName),
  224. onChange: (value: any) => {
  225. this.props.onChange(field, value);
  226. },
  227. };
  228. }
  229. return (
  230. <FieldInputStyled
  231. layout={this.props.layout || "page"}
  232. key={field.name}
  233. name={field.name}
  234. type={field.type}
  235. minimum={field.minimum}
  236. maximum={field.maximum}
  237. label={field.label || LabelDictionary.get(field.name)}
  238. description={field.description || LabelDictionary.getDescription(field.name)}
  239. password={field.name.toLowerCase().includes("password")}
  240. enum={field.enum}
  241. addNullValue
  242. required={field.required}
  243. width={this.props.fieldWidth || ThemeProps.inputSizes.wizard.width}
  244. nullableBoolean={field.nullableBoolean}
  245. disabled={field.disabled}
  246. // eslint-disable-next-line react/jsx-props-no-spreading
  247. {...additionalProps}
  248. />
  249. );
  250. }
  251. renderOptionsFields() {
  252. let fieldsSchema: Field[] = this.getDefaultSimpleFieldsSchema();
  253. const isRequired = (f: Field) =>
  254. f.required || f.properties?.some(p => p.required);
  255. const defaultFieldNames = fieldsSchema.map(f => f.name);
  256. const filteredOptions = this.props.options.filter(
  257. f => !defaultFieldNames.includes(f.name) && isRequired(f)
  258. );
  259. fieldsSchema = fieldsSchema.concat(filteredOptions);
  260. const nonNullableBooleans: string[] = fieldsSchema
  261. .filter(f => f.type === "boolean" && f.nullableBoolean === false)
  262. .map(f => f.name);
  263. let executeNowColumn: number;
  264. const fields: FieldRender[] = fieldsSchema
  265. .filter(f => shouldRenderField(f))
  266. .map((field, i) => {
  267. let column: number = i % 2;
  268. if (field.name === "execute_now") {
  269. executeNowColumn = column;
  270. }
  271. const usableField = toJS(field);
  272. if (
  273. field.type === "boolean" &&
  274. !nonNullableBooleans.find(name => name === field.name)
  275. ) {
  276. usableField.nullableBoolean = true;
  277. }
  278. return {
  279. column,
  280. component: this.renderOptionsField(usableField),
  281. field: usableField,
  282. };
  283. });
  284. const groups = this.generateGroups(fields);
  285. return (
  286. <Fields ref={this.props.onScrollableRef} layout={this.props.layout}>
  287. {groups.map((g, i) => {
  288. const getColumnInGroup = (field: any, fieldIndex: number) =>
  289. g.name ? fieldIndex % 2 : field.column;
  290. return (
  291. <Group key={g.name || 0}>
  292. {g.name ? (
  293. <GroupName>
  294. <GroupNameBar />
  295. <GroupNameText>{LabelDictionary.get(g.name)}</GroupNameText>
  296. <GroupNameBar />
  297. </GroupName>
  298. ) : null}
  299. <GroupFields>
  300. <Column left>
  301. {g.fields.map(
  302. (f, j) => getColumnInGroup(f, j) === 0 && f.component
  303. )}
  304. </Column>
  305. <Column right>
  306. {g.fields.map(
  307. (f, j) => getColumnInGroup(f, j) === 1 && f.component
  308. )}
  309. </Column>
  310. </GroupFields>
  311. </Group>
  312. );
  313. })}
  314. </Fields>
  315. );
  316. }
  317. render() {
  318. return (
  319. <Wrapper>
  320. {this.renderOptionsFields()}
  321. </Wrapper>
  322. );
  323. }
  324. }
  325. export default WizardExecuteOptions;