AssessmentDetailsContent.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  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, { css } from "styled-components";
  17. import AssessedVmListItem from "@src/components/modules/AssessmentModule/AssessedVmListItem";
  18. import DetailsNavigation from "@src/components/modules/NavigationModule/DetailsNavigation";
  19. import { ThemePalette, ThemeProps } from "@src/components/Theme";
  20. import Button from "@src/components/ui/Button";
  21. import Checkbox from "@src/components/ui/Checkbox";
  22. import DropdownFilter from "@src/components/ui/Dropdowns/DropdownFilter";
  23. import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink";
  24. import SmallLoading from "@src/components/ui/SmallLoading";
  25. import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
  26. import Table from "@src/components/ui/Table";
  27. import DateUtils from "@src/utils/DateUtils";
  28. import arrowImage from "./images/arrow.svg";
  29. import azureMigrateImage from "./images/logo.svg";
  30. import type { Assessment, VmItem, AzureLocation } from "@src/@types/Assessment";
  31. import type { Endpoint } from "@src/@types/Endpoint";
  32. import type { Instance, Nic } from "@src/@types/Instance";
  33. import type { Network, NetworkMap } from "@src/@types/Network";
  34. const Wrapper = styled.div<any>`
  35. display: flex;
  36. justify-content: center;
  37. `;
  38. const Buttons = styled.div<any>`
  39. margin-top: 46px;
  40. display: flex;
  41. flex-direction: column;
  42. button:first-child {
  43. margin-bottom: 16px;
  44. }
  45. `;
  46. const DetailsBody = styled.div<any>`
  47. ${ThemeProps.exactWidth(ThemeProps.contentWidth)}
  48. margin-bottom: 32px;
  49. `;
  50. const Columns = styled.div<any>`
  51. display: flex;
  52. margin-left: -32px;
  53. `;
  54. const Column = styled.div<any>`
  55. width: 50%;
  56. margin-left: 32px;
  57. `;
  58. const Row = styled.div<any>`
  59. margin-bottom: 32px;
  60. `;
  61. const Field = styled.div<any>`
  62. display: flex;
  63. flex-direction: column;
  64. `;
  65. const Label = styled.div<any>`
  66. font-size: 10px;
  67. color: ${ThemePalette.grayscale[3]};
  68. font-weight: ${ThemeProps.fontWeights.medium};
  69. text-transform: uppercase;
  70. `;
  71. const Value = styled.div<any>`
  72. display: ${props => (props.flex ? "flex" : "inline-table")};
  73. margin-top: 3px;
  74. ${props => (props.capitalize ? "text-transform: capitalize;" : "")}
  75. `;
  76. const AzureMigrateLogo = styled.div<any>`
  77. width: 208px;
  78. height: 32px;
  79. background: url("${azureMigrateImage}") center no-repeat;
  80. `;
  81. const LoadingWrapper = styled.div<any>`
  82. display: flex;
  83. flex-direction: column;
  84. align-items: center;
  85. margin: 32px 0;
  86. `;
  87. const LoadingText = styled.div<any>`
  88. font-size: 18px;
  89. margin-top: 32px;
  90. `;
  91. const SmallLoadingWrapper = styled.div<any>`
  92. display: flex;
  93. align-items: center;
  94. justify-content: center;
  95. `;
  96. const SmallLoadingText = styled.div<any>`
  97. font-size: 14px;
  98. margin-left: 16px;
  99. `;
  100. const TableStyled = styled(Table)<any>`
  101. margin-top: 62px;
  102. ${props =>
  103. props.addWidthPadding
  104. ? css`
  105. margin-left: -24px;
  106. &:after {
  107. margin-left: 24px;
  108. }
  109. `
  110. : ""}
  111. `;
  112. const TableHeaderStyle = css`
  113. margin-left: 24px;
  114. `;
  115. const TableBodyStyle = css`
  116. padding-left: 24px;
  117. `;
  118. const NetworkItem = styled.div<any>`
  119. display: flex;
  120. width: 100%;
  121. `;
  122. const column = () => css`
  123. padding-right: 32px;
  124. width: 100%;
  125. max-width: 25%;
  126. `;
  127. const NetworkName = styled.div<any>`
  128. ${column()}
  129. `;
  130. const Arrow = styled.div<any>`
  131. width: 32px;
  132. height: 16px;
  133. position: absolute;
  134. right: 0;
  135. background: url("${arrowImage}") no-repeat;
  136. background-position-y: center;
  137. `;
  138. const ColumnStub = styled.div<any>`
  139. ${column()}
  140. position: relative;
  141. &:last-child {
  142. padding-right: 0;
  143. }
  144. `;
  145. const VmHeaderItem = styled.div<any>`
  146. display: flex;
  147. font-size: 14px;
  148. `;
  149. const VmHeaderItemLabel = styled.div<any>`
  150. font-size: 10px;
  151. margin-left: 8px;
  152. `;
  153. const NavigationItems = [
  154. {
  155. label: "Details",
  156. value: "",
  157. },
  158. ];
  159. type Props = {
  160. item: Assessment | null;
  161. detailsLoading: boolean;
  162. instancesDetailsLoading: boolean;
  163. instancesLoading: boolean;
  164. networksLoading: boolean;
  165. instancesDetailsProgress: number | null;
  166. targetEndpoint: Endpoint;
  167. targetEndpoints: Endpoint[];
  168. onTargetEndpointChange: (endpoint: Endpoint) => void;
  169. targetEndpointsLoading: boolean;
  170. sourceEndpoints: Endpoint[];
  171. sourceEndpoint: Endpoint | null;
  172. sourceEndpointsLoading: boolean;
  173. locations: AzureLocation[];
  174. selectedLocation: string | null;
  175. onLocationChange: (locationName: string) => void;
  176. selectedResourceGroup: string;
  177. resourceGroups: string[];
  178. onResourceGroupChange: (resourceGroupName: string) => void;
  179. targetOptionsLoading: boolean;
  180. assessedVmsCount: number;
  181. filteredAssessedVms: VmItem[];
  182. selectedVms: string[];
  183. instancesDetails: Instance[];
  184. instances: Instance[];
  185. loadingVmSizes: boolean;
  186. vmSizes: string[];
  187. onVmSizeChange: (vmId: string, size: string) => void;
  188. onGetSelectedVmSize: (vm: VmItem) => string | null;
  189. networks: Network[];
  190. page: string;
  191. onSourceEndpointChange: (endpoint: Endpoint) => void;
  192. onVmSearchValueChange: (value: string) => void;
  193. vmSearchValue: string;
  194. onVmSelectedChange: (vm: VmItem, selected: boolean) => void;
  195. onNetworkChange: (sourceNic: Nic, targetNetwork: Network) => void;
  196. onRefresh: () => void;
  197. onMigrateClick: () => void;
  198. selectedNetworks: NetworkMap[];
  199. selectAllVmsChecked: boolean;
  200. onSelectAllVmsChange: (selected: boolean) => void;
  201. };
  202. @observer
  203. class AssessmentDetailsContent extends React.Component<Props> {
  204. static defaultProps = {
  205. page: "",
  206. };
  207. doesVmMatchSource(vm: VmItem) {
  208. if (
  209. !this.props.sourceEndpoint ||
  210. !this.props.sourceEndpoint.connection_info
  211. ) {
  212. return false;
  213. }
  214. if (
  215. this.props.instances.length > 0 &&
  216. !this.props.instances.find(
  217. i =>
  218. i.name === vm.properties.displayName ||
  219. i.instance_name === vm.properties.displayName
  220. )
  221. ) {
  222. return false;
  223. }
  224. return (
  225. this.props.sourceEndpoint.connection_info.host ===
  226. vm.properties.datacenterManagementServerName
  227. );
  228. }
  229. renderMainDetails() {
  230. if (this.props.page !== "" || !this.props.item || !this.props.item.id) {
  231. return null;
  232. }
  233. const status = this.props.item
  234. ? this.props.item.properties.status === "Completed"
  235. ? "Ready for Migration"
  236. : this.props.item.properties.status
  237. : "";
  238. const locationItem: AzureLocation | undefined = this.props.locations.find(
  239. l => l.id === this.props.selectedLocation
  240. );
  241. return (
  242. <Columns>
  243. <Column>
  244. <Row>
  245. <AzureMigrateLogo />
  246. </Row>
  247. <Row>
  248. <Field>
  249. <Label>Last Update</Label>
  250. <Value>
  251. {this.props.item
  252. ? DateUtils.getLocalDate(
  253. this.props.item.properties.updatedTimestamp
  254. ).toFormat("yyyy-LL-dd HH:mm:ss")
  255. : "-"}
  256. </Value>
  257. </Field>
  258. </Row>
  259. <Row>
  260. <Field>
  261. <Label>Migration Project</Label>
  262. <Value>
  263. {this.props.item ? this.props.item.projectName : ""}
  264. </Value>
  265. </Field>
  266. </Row>
  267. <Row>
  268. <Field>
  269. <Label>VM Group</Label>
  270. <Value>{this.props.item ? this.props.item.groupName : ""}</Value>
  271. </Field>
  272. </Row>
  273. <Row>
  274. <Field>
  275. <Label>Status</Label>
  276. <Value>{status}</Value>
  277. </Field>
  278. </Row>
  279. </Column>
  280. <Column>
  281. <Row>
  282. <Field>
  283. <Label>Source Endpoint</Label>
  284. <Value>
  285. <DropdownLink
  286. selectedItem={
  287. this.props.sourceEndpoint
  288. ? this.props.sourceEndpoint.id
  289. : ""
  290. }
  291. items={this.props.sourceEndpoints.map(endpoint => ({
  292. label: endpoint.name,
  293. value: endpoint.id,
  294. endpoint,
  295. }))}
  296. onChange={item => {
  297. this.props.onSourceEndpointChange(item.endpoint);
  298. }}
  299. selectItemLabel="Select Endpoint"
  300. noItemsLabel={
  301. this.props.sourceEndpointsLoading
  302. ? "Loading ...."
  303. : "No matching endpoints"
  304. }
  305. />
  306. </Value>
  307. </Field>
  308. </Row>
  309. <Row>
  310. <Field>
  311. <Label>Target endpoint</Label>
  312. <Value>
  313. <DropdownLink
  314. selectedItem={
  315. this.props.targetEndpoint
  316. ? this.props.targetEndpoint.id
  317. : ""
  318. }
  319. items={this.props.targetEndpoints.map(endpoint => ({
  320. label: endpoint.name,
  321. value: endpoint.id,
  322. endpoint,
  323. }))}
  324. onChange={item => {
  325. this.props.onTargetEndpointChange(item.endpoint);
  326. }}
  327. selectItemLabel="Select Endpoint"
  328. noItemsLabel={
  329. this.props.targetEndpointsLoading
  330. ? "Loading ...."
  331. : "No Azure endpoints"
  332. }
  333. />
  334. </Value>
  335. </Field>
  336. </Row>
  337. <Row>
  338. <Field>
  339. <Label>Resource Group</Label>
  340. <Value>
  341. <DropdownLink
  342. selectedItem={this.props.selectedResourceGroup}
  343. items={this.props.resourceGroups.map(group => ({
  344. label: group,
  345. value: group,
  346. }))}
  347. onChange={item => {
  348. this.props.onResourceGroupChange(item.value);
  349. }}
  350. noItemsLabel={
  351. this.props.targetOptionsLoading
  352. ? "Loading ...."
  353. : "No Resource Groups found"
  354. }
  355. />
  356. </Value>
  357. </Field>
  358. </Row>
  359. <Row>
  360. <Field>
  361. <Label>Location</Label>
  362. <Value>
  363. <DropdownLink
  364. selectedItem={locationItem ? locationItem.id : ""}
  365. items={this.props.locations.map(location => ({
  366. label: location.name,
  367. value: location.id,
  368. }))}
  369. onChange={item => {
  370. this.props.onLocationChange(item.value);
  371. }}
  372. noItemsLabel={
  373. this.props.targetOptionsLoading
  374. ? "Loading ...."
  375. : "No Locations found"
  376. }
  377. />
  378. </Value>
  379. </Field>
  380. </Row>
  381. </Column>
  382. </Columns>
  383. );
  384. }
  385. renderVmsTable() {
  386. const loading = this.props.instancesLoading;
  387. const items = this.props.filteredAssessedVms.map(vm => (
  388. <AssessedVmListItem
  389. key={vm.id}
  390. item={vm}
  391. selected={
  392. this.props.selectedVms.filter(m => m === vm.properties.displayName)
  393. .length > 0
  394. }
  395. onSelectedChange={(selectedVm, selected) => {
  396. this.props.onVmSelectedChange(selectedVm, selected);
  397. }}
  398. disabled={!this.doesVmMatchSource(vm)}
  399. loadingVmSizes={this.props.loadingVmSizes}
  400. recommendedVmSize={vm.properties.recommendedSize}
  401. vmSizes={this.props.vmSizes}
  402. selectedVmSize={this.props.onGetSelectedVmSize(vm)}
  403. onVmSizeChange={size => {
  404. this.props.onVmSizeChange(vm.properties.displayName, size);
  405. }}
  406. />
  407. ));
  408. const vmCountLabel = `(${
  409. this.props.filteredAssessedVms.length === this.props.assessedVmsCount
  410. ? this.props.assessedVmsCount
  411. : `${this.props.filteredAssessedVms.length} OUT OF ${this.props.assessedVmsCount}`
  412. })`;
  413. const vmHeaderItem = (
  414. <VmHeaderItem>
  415. {loading ? null : (
  416. <Checkbox
  417. checked={this.props.selectAllVmsChecked}
  418. onChange={checked => {
  419. this.props.onSelectAllVmsChange(checked);
  420. }}
  421. />
  422. )}
  423. <VmHeaderItemLabel>Virtual Machine {vmCountLabel}</VmHeaderItemLabel>
  424. <DropdownFilter
  425. searchPlaceholder="Filter Virtual Machines"
  426. searchValue={this.props.vmSearchValue}
  427. onSearchChange={value => {
  428. this.props.onVmSearchValueChange(value);
  429. }}
  430. />
  431. </VmHeaderItem>
  432. );
  433. return (
  434. <TableStyled
  435. addWidthPadding
  436. items={loading ? [] : items}
  437. bodyStyle={TableBodyStyle}
  438. headerStyle={TableHeaderStyle}
  439. header={[vmHeaderItem, "OS", "Target Disk Type", "Azure VM Size"]}
  440. useSecondaryStyle
  441. noItemsComponent={this.renderLoading(
  442. "Loading instances, please wait ..."
  443. )}
  444. />
  445. );
  446. }
  447. renderNetworkTable() {
  448. const loading =
  449. this.props.networksLoading || this.props.instancesDetailsLoading;
  450. if (loading) {
  451. return (
  452. <TableStyled
  453. items={[]}
  454. header={["Source Network", "", "", "Target Network"]}
  455. useSecondaryStyle
  456. noItemsStyle={{ marginLeft: 0 }}
  457. noItemsComponent={this.renderNetworksLoading()}
  458. />
  459. );
  460. }
  461. const nics: Nic[] = [];
  462. this.props.instancesDetails.forEach(instance => {
  463. if (!instance.devices || !instance.devices.nics) {
  464. return;
  465. }
  466. instance.devices.nics.forEach(nic => {
  467. if (nics.find(n => n.network_name === nic.network_name)) {
  468. return;
  469. }
  470. nics.push(nic);
  471. });
  472. });
  473. if (nics.length === 0) {
  474. return null;
  475. }
  476. const items = nics.map(nic => {
  477. let selectedNetworkName: string | undefined;
  478. const selectedNetwork =
  479. this.props.selectedNetworks &&
  480. this.props.selectedNetworks.find(
  481. n => n.sourceNic.network_name === nic.network_name
  482. );
  483. if (selectedNetwork) {
  484. selectedNetworkName = selectedNetwork.targetNetwork?.name;
  485. }
  486. return (
  487. <NetworkItem key={nic.network_name}>
  488. <NetworkName width="25%">{nic.network_name}</NetworkName>
  489. <ColumnStub width="25%">
  490. <Arrow />
  491. </ColumnStub>
  492. <ColumnStub width="25%" />
  493. <ColumnStub width="25%">
  494. <DropdownLink
  495. width="208px"
  496. noItemsLabel="No Networks found"
  497. selectItemLabel="Select Network"
  498. selectedItem={selectedNetworkName}
  499. onChange={item => {
  500. this.props.onNetworkChange(nic, item.network);
  501. }}
  502. items={this.props.networks.map(network => ({
  503. value: network.name || "",
  504. label: network.name || "",
  505. network,
  506. }))}
  507. />
  508. </ColumnStub>
  509. </NetworkItem>
  510. );
  511. });
  512. return (
  513. <TableStyled
  514. items={loading ? [] : items}
  515. header={["Source Network", "", "", "Target Network"]}
  516. useSecondaryStyle
  517. noItemsStyle={{ marginLeft: 0 }}
  518. noItemsComponent={this.renderNetworksLoading()}
  519. />
  520. );
  521. }
  522. renderNetworksLoading() {
  523. let loadingProgress = -1;
  524. if (this.props.instancesDetailsLoading) {
  525. if (this.props.instancesDetailsProgress != null) {
  526. loadingProgress = Math.round(this.props.instancesDetailsProgress * 100);
  527. }
  528. }
  529. return (
  530. <SmallLoadingWrapper>
  531. <SmallLoading loadingProgress={loadingProgress} />
  532. <SmallLoadingText>Loading networks, please wait ...</SmallLoadingText>
  533. </SmallLoadingWrapper>
  534. );
  535. }
  536. renderButtons() {
  537. return (
  538. <Buttons>
  539. <Button secondary onClick={this.props.onRefresh}>
  540. Refresh
  541. </Button>
  542. <Button
  543. disabled={
  544. this.props.selectedVms.length === 0 ||
  545. this.props.selectedNetworks.length === 0
  546. }
  547. onClick={() => {
  548. this.props.onMigrateClick();
  549. }}
  550. >
  551. Migrate / Replicate
  552. </Button>
  553. </Buttons>
  554. );
  555. }
  556. renderLoading(message: string) {
  557. return (
  558. <LoadingWrapper>
  559. <StatusImage loading />
  560. <LoadingText>{message}</LoadingText>
  561. </LoadingWrapper>
  562. );
  563. }
  564. render() {
  565. return (
  566. <Wrapper>
  567. <DetailsNavigation
  568. items={NavigationItems}
  569. selectedValue={this.props.page}
  570. itemId={this.props.item ? this.props.item.id : ""}
  571. customHref={() => "#"}
  572. />
  573. <DetailsBody>
  574. {this.props.detailsLoading ? null : this.renderMainDetails()}
  575. {this.props.detailsLoading
  576. ? this.renderLoading("Loading assessment...")
  577. : null}
  578. {this.props.detailsLoading ? null : this.renderVmsTable()}
  579. {this.props.detailsLoading || this.props.instancesLoading
  580. ? null
  581. : this.renderNetworkTable()}
  582. {this.props.detailsLoading ? null : this.renderButtons()}
  583. </DetailsBody>
  584. </Wrapper>
  585. );
  586. }
  587. }
  588. export default AssessmentDetailsContent;