ReplicaMigrationOptions.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /*
  2. Copyright (C) 2017 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 { observer } from "mobx-react";
  15. import React from "react";
  16. import styled from "styled-components";
  17. import { TransferItemDetails } from "@src/@types/MainItem";
  18. import { MinionPool } from "@src/@types/MinionPool";
  19. import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from "@src/components/modules/WizardModule/WizardOptions";
  20. import WizardScripts from "@src/components/modules/WizardModule/WizardScripts";
  21. import { ThemeProps } from "@src/components/Theme";
  22. import Button from "@src/components/ui/Button";
  23. import FieldInput from "@src/components/ui/FieldInput";
  24. import LoadingButton from "@src/components/ui/LoadingButton";
  25. import ToggleButtonBar from "@src/components/ui/ToggleButtonBar";
  26. import KeyboardManager from "@src/utils/KeyboardManager";
  27. import LabelDictionary from "@src/utils/LabelDictionary";
  28. import replicaMigrationImage from "./images/replica-migration.svg";
  29. import replicaMigrationFields from "./replicaMigrationFields";
  30. import type { Field } from "@src/@types/Field";
  31. import type { Instance, InstanceScript } from "@src/@types/Instance";
  32. const Wrapper = styled.div<any>`
  33. display: flex;
  34. flex-direction: column;
  35. align-items: center;
  36. padding: 0 32px 32px 32px;
  37. min-height: 0;
  38. `;
  39. const Image = styled.div<any>`
  40. ${ThemeProps.exactWidth("288px")}
  41. ${ThemeProps.exactHeight("96px")}
  42. background: url('${replicaMigrationImage}') center no-repeat;
  43. margin: 80px 0;
  44. `;
  45. const OptionsBody = styled.div<any>`
  46. display: flex;
  47. flex-direction: column;
  48. `;
  49. const ScriptsBody = styled.div<any>`
  50. display: flex;
  51. flex-direction: column;
  52. width: 100%;
  53. overflow: auto;
  54. min-height: 0;
  55. margin-bottom: 32px;
  56. `;
  57. const Form = styled.div<any>`
  58. display: flex;
  59. flex-wrap: wrap;
  60. margin-left: -64px;
  61. justify-content: space-between;
  62. margin: 0 auto;
  63. `;
  64. const Buttons = styled.div<any>`
  65. display: flex;
  66. justify-content: space-between;
  67. width: 100%;
  68. `;
  69. const FieldInputStyled = styled(FieldInput)`
  70. width: 224px;
  71. justify-content: space-between;
  72. margin-bottom: 32px;
  73. `;
  74. type Props = {
  75. instances: Instance[];
  76. transferItem: TransferItemDetails | null;
  77. minionPools: MinionPool[];
  78. loadingInstances: boolean;
  79. defaultSkipOsMorphing?: boolean | null;
  80. migrating?: boolean;
  81. onCancelClick: () => void;
  82. onMigrateClick: (opts: {
  83. fields: Field[];
  84. uploadedUserScripts: InstanceScript[];
  85. removedUserScripts: InstanceScript[];
  86. minionPoolMappings: { [instance: string]: string };
  87. }) => void;
  88. onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void;
  89. };
  90. type State = {
  91. fields: Field[];
  92. selectedBarButton: string;
  93. uploadedScripts: InstanceScript[];
  94. removedScripts: InstanceScript[];
  95. minionPoolMappings: { [instance: string]: string };
  96. };
  97. @observer
  98. class ReplicaMigrationOptions extends React.Component<Props, State> {
  99. state: State = {
  100. fields: [],
  101. selectedBarButton: "options",
  102. uploadedScripts: [],
  103. removedScripts: [],
  104. minionPoolMappings: {},
  105. };
  106. scrollableRef!: HTMLElement;
  107. UNSAFE_componentWillMount() {
  108. const mappings =
  109. this.props.transferItem?.instance_osmorphing_minion_pool_mappings || {};
  110. this.setState({
  111. fields: replicaMigrationFields.map(f =>
  112. f.name === "skip_os_morphing"
  113. ? { ...f, value: this.props.defaultSkipOsMorphing || null }
  114. : f
  115. ),
  116. minionPoolMappings: { ...mappings },
  117. });
  118. }
  119. componentDidMount() {
  120. KeyboardManager.onEnter(
  121. "migration-options",
  122. () => {
  123. this.migrate();
  124. },
  125. 2
  126. );
  127. }
  128. componentDidUpdate(_: Props, prevState: State) {
  129. if (prevState.selectedBarButton !== this.state.selectedBarButton) {
  130. if (this.props.onResizeUpdate) {
  131. this.props.onResizeUpdate(this.scrollableRef, 0);
  132. }
  133. }
  134. }
  135. componentWillUnmount() {
  136. KeyboardManager.removeKeyDown("migration-options");
  137. }
  138. migrate() {
  139. this.props.onMigrateClick({
  140. fields: this.state.fields,
  141. uploadedUserScripts: this.state.uploadedScripts,
  142. removedUserScripts: this.state.removedScripts,
  143. minionPoolMappings: this.state.minionPoolMappings,
  144. });
  145. }
  146. handleValueChange(field: Field, value: boolean) {
  147. this.setState(prevState => {
  148. const fields = prevState.fields.map(f => {
  149. const newField = { ...f };
  150. if (f.name === field.name) {
  151. newField.value = value;
  152. }
  153. return newField;
  154. });
  155. return { fields };
  156. });
  157. }
  158. handleCancelScript(global: string | null, instanceName: string | null) {
  159. this.setState(prevState => ({
  160. uploadedScripts: prevState.uploadedScripts.filter(s =>
  161. global ? s.global !== global : s.instanceId !== instanceName
  162. ),
  163. }));
  164. }
  165. handleScriptUpload(script: InstanceScript) {
  166. this.setState(prevState => ({
  167. uploadedScripts: [...prevState.uploadedScripts, script],
  168. }));
  169. }
  170. handleScriptRemove(script: InstanceScript) {
  171. this.setState(prevState => ({
  172. removedScripts: [...prevState.removedScripts, script],
  173. }));
  174. }
  175. renderField(field: Field) {
  176. return (
  177. <FieldInputStyled
  178. width={224}
  179. key={field.name}
  180. name={field.name}
  181. type={field.type}
  182. value={field.value || field.default}
  183. minimum={field.minimum}
  184. maximum={field.maximum}
  185. layout="page"
  186. label={field.label || LabelDictionary.get(field.name)}
  187. onChange={value => this.handleValueChange(field, value)}
  188. description={LabelDictionary.getDescription(field.name)}
  189. />
  190. );
  191. }
  192. renderMinionPoolMappings() {
  193. const minionPools = this.props.minionPools.filter(
  194. m => m.endpoint_id === this.props.transferItem?.destination_endpoint_id
  195. );
  196. if (!minionPools.length) {
  197. return null;
  198. }
  199. const properties: Field[] = this.props.instances.map(instance => ({
  200. name: instance.instance_name || instance.id,
  201. label: instance.name,
  202. type: "string",
  203. enum: minionPools.map(minionPool => ({
  204. name: minionPool.name,
  205. id: minionPool.id,
  206. })),
  207. }));
  208. return (
  209. <FieldInputStyled
  210. width={500}
  211. style={{ marginBottom: "64px" }}
  212. name={INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS}
  213. type="object"
  214. valueCallback={field =>
  215. this.state.minionPoolMappings &&
  216. this.state.minionPoolMappings[field.name]
  217. }
  218. layout="page"
  219. label="Instance OSMorphing Minion Pool Mappings"
  220. onChange={(value, field) =>
  221. this.setState(prevState => {
  222. const minionPoolMappings = { ...prevState.minionPoolMappings };
  223. minionPoolMappings[field!.name] = value;
  224. return { minionPoolMappings };
  225. })
  226. }
  227. properties={properties}
  228. labelRenderer={(propName: string) =>
  229. propName.indexOf("/") > -1
  230. ? propName.split("/")[propName.split("/").length - 1]
  231. : propName
  232. }
  233. />
  234. );
  235. }
  236. renderOptions() {
  237. return (
  238. <>
  239. <Form>{this.state.fields.map(field => this.renderField(field))}</Form>
  240. {this.renderMinionPoolMappings()}
  241. </>
  242. );
  243. }
  244. renderScripts() {
  245. return (
  246. <WizardScripts
  247. instances={this.props.instances}
  248. loadingInstances={this.props.loadingInstances}
  249. onScriptUpload={s => {
  250. this.handleScriptUpload(s);
  251. }}
  252. onScriptDataRemove={s => {
  253. this.handleScriptRemove(s);
  254. }}
  255. onCancelScript={(g, i) => {
  256. this.handleCancelScript(g, i);
  257. }}
  258. uploadedScripts={this.state.uploadedScripts}
  259. removedScripts={this.state.removedScripts}
  260. userScriptData={this.props.transferItem?.user_scripts}
  261. scrollableRef={(r: HTMLElement) => {
  262. this.scrollableRef = r;
  263. }}
  264. layout="modal"
  265. />
  266. );
  267. }
  268. renderBody() {
  269. const Body =
  270. this.state.selectedBarButton === "options" ? OptionsBody : ScriptsBody;
  271. return (
  272. <Body>
  273. <ToggleButtonBar
  274. items={[
  275. { label: "Options", value: "options" },
  276. { label: "User Scripts", value: "script" },
  277. ]}
  278. selectedValue={this.state.selectedBarButton}
  279. onChange={item => {
  280. this.setState({ selectedBarButton: item.value });
  281. }}
  282. style={{ marginBottom: "32px" }}
  283. />
  284. {this.state.selectedBarButton === "options"
  285. ? this.renderOptions()
  286. : this.renderScripts()}
  287. </Body>
  288. );
  289. }
  290. render() {
  291. return (
  292. <Wrapper>
  293. <Image />
  294. {this.renderBody()}
  295. <Buttons>
  296. <Button secondary onClick={this.props.onCancelClick}>
  297. Cancel
  298. </Button>
  299. {this.props.migrating ? (
  300. <LoadingButton>Migrating ...</LoadingButton>
  301. ) : (
  302. <Button
  303. onClick={() => {
  304. this.migrate();
  305. }}
  306. >
  307. Migrate
  308. </Button>
  309. )}
  310. </Buttons>
  311. </Wrapper>
  312. );
  313. }
  314. }
  315. export default ReplicaMigrationOptions;