MainDetails.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  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 * as React from "react";
  16. import { Link } from "react-router";
  17. import styled, { css } from "styled-components";
  18. import fieldHelper from "@src/@types/Field";
  19. import { ActionItem } from "@src/@types/MainItem";
  20. import { MinionPool } from "@src/@types/MinionPool";
  21. import EndpointLogos from "@src/components/modules/EndpointModule/EndpointLogos";
  22. import TransferDetailsTable from "@src/components/modules/TransferModule/TransferDetailsTable";
  23. import { ThemePalette, ThemeProps } from "@src/components/Theme";
  24. import CopyValue from "@src/components/ui/CopyValue";
  25. import PasswordValue from "@src/components/ui/PasswordValue";
  26. import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon";
  27. import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
  28. import DateUtils from "@src/utils/DateUtils";
  29. import LabelDictionary from "@src/utils/LabelDictionary";
  30. import arrowImage from "./images/arrow.svg";
  31. import type { Instance } from "@src/@types/Instance";
  32. import type { Endpoint, StorageBackend } from "@src/@types/Endpoint";
  33. import type { Network } from "@src/@types/Network";
  34. import type { Field as FieldType } from "@src/@types/Field";
  35. const Wrapper = styled.div<any>`
  36. display: flex;
  37. flex-direction: column;
  38. padding-bottom: 48px;
  39. `;
  40. const WarningWrapper = styled.div`
  41. display: flex;
  42. background: ${ThemePalette.warning}66;
  43. padding: 8px;
  44. border-radius: 4px;
  45. margin-bottom: 24px;
  46. align-items: center;
  47. `;
  48. const WarningText = styled.div`
  49. margin-left: 8px;
  50. `;
  51. const ColumnsLayout = styled.div<any>`
  52. display: flex;
  53. `;
  54. const Column = styled.div<any>`
  55. ${props => ThemeProps.exactWidth(props.width)}
  56. `;
  57. const Arrow = styled.div<any>`
  58. width: 34px;
  59. height: 24px;
  60. background: url("${arrowImage}") center no-repeat;
  61. margin-top: 84px;
  62. `;
  63. const Row = styled.div<any>`
  64. margin-bottom: 32px;
  65. &:last-child {
  66. margin-bottom: 16px;
  67. }
  68. `;
  69. const Field = styled.div<any>`
  70. display: flex;
  71. flex-direction: column;
  72. `;
  73. const Label = styled.div<any>`
  74. font-size: 10px;
  75. color: ${ThemePalette.grayscale[3]};
  76. font-weight: ${ThemeProps.fontWeights.medium};
  77. text-transform: uppercase;
  78. display: flex;
  79. align-items: center;
  80. `;
  81. const StatusIconStub = styled.div<any>`
  82. ${ThemeProps.exactSize("16px")}
  83. `;
  84. const Value = styled.div<any>`
  85. display: ${props =>
  86. props.flex ? "flex" : props.block ? "block" : "inline-table"};
  87. margin-top: 3px;
  88. ${props => (props.capitalize ? "text-transform: capitalize;" : "")}
  89. `;
  90. const ValueLink = styled(Link)`
  91. display: flex;
  92. margin-top: 3px;
  93. color: ${ThemePalette.primary};
  94. text-decoration: none;
  95. cursor: pointer;
  96. `;
  97. const Loading = styled.div<any>`
  98. display: flex;
  99. justify-content: center;
  100. align-items: center;
  101. height: 200px;
  102. `;
  103. const PropertiesTable = styled.div<any>``;
  104. const PropertyRow = styled.div<any>`
  105. display: flex;
  106. justify-content: space-between;
  107. margin-bottom: 4px;
  108. `;
  109. const PropertyText = css``;
  110. const PropertyName = styled.div<any>`
  111. ${PropertyText}
  112. overflow: hidden;
  113. text-overflow: ellipsis;
  114. max-width: 50%;
  115. `;
  116. const PropertyValue = styled.div<any>`
  117. ${PropertyText}
  118. color: ${ThemePalette.grayscale[4]};
  119. text-align: right;
  120. overflow: hidden;
  121. text-overflow: ellipsis;
  122. max-width: calc(50% + 16px);
  123. margin-right: -16px;
  124. `;
  125. type Props = {
  126. item?: ActionItem | null;
  127. minionPools: MinionPool[];
  128. storageBackends: StorageBackend[];
  129. destinationSchema: FieldType[];
  130. destinationSchemaLoading: boolean;
  131. sourceSchema: FieldType[];
  132. sourceSchemaLoading: boolean;
  133. instancesDetails: Instance[];
  134. instancesDetailsLoading: boolean;
  135. endpoints: Endpoint[];
  136. networks?: Network[];
  137. bottomControls: React.ReactNode;
  138. loading: boolean;
  139. };
  140. type State = {
  141. showPassword: string[];
  142. };
  143. @observer
  144. class MainDetails extends React.Component<Props, State> {
  145. state = {
  146. showPassword: [],
  147. };
  148. getSourceEndpoint(): Endpoint | undefined {
  149. const endpoint = this.props.endpoints.find(
  150. e => this.props.item && e.id === this.props.item.origin_endpoint_id,
  151. );
  152. return endpoint;
  153. }
  154. getDestinationEndpoint(): Endpoint | undefined {
  155. const endpoint = this.props.endpoints.find(
  156. e => this.props.item && e.id === this.props.item.destination_endpoint_id,
  157. );
  158. return endpoint;
  159. }
  160. renderLastExecutionTime() {
  161. return this.props.item
  162. ? this.renderValue(DateUtils.formatSafeDate(this.props.item.updated_at))
  163. : "-";
  164. }
  165. renderValue(value: string) {
  166. return <CopyValue value={value} maxWidth="90%" />;
  167. }
  168. renderEndpointLink(type: string): React.ReactNode {
  169. const endpointIsMissing = (
  170. <Value flex>
  171. <StatusIcon style={{ marginRight: "8px" }} status="ERROR" />
  172. Endpoint is missing
  173. </Value>
  174. );
  175. const endpoint =
  176. type === "source"
  177. ? this.getSourceEndpoint()
  178. : this.getDestinationEndpoint();
  179. if (endpoint) {
  180. return (
  181. <ValueLink to={`/endpoints/${endpoint.id}`}>{endpoint.name}</ValueLink>
  182. );
  183. }
  184. return endpointIsMissing;
  185. }
  186. renderPropertiesTable(
  187. propertyNames: string[],
  188. type: "source" | "destination",
  189. ) {
  190. const endpoint =
  191. type === "source"
  192. ? this.getSourceEndpoint()
  193. : this.getDestinationEndpoint();
  194. const getValue = (name: string, value: any) => {
  195. if (
  196. value.join &&
  197. value.length &&
  198. value[0].destination &&
  199. value[0].source
  200. ) {
  201. return value
  202. .map(
  203. (v: { source: any; destination: any }) =>
  204. `${v.source}=${v.destination}`,
  205. )
  206. .join(", ");
  207. }
  208. const schema =
  209. type === "source"
  210. ? this.props.sourceSchema
  211. : this.props.destinationSchema;
  212. return fieldHelper.getValueAlias({
  213. name,
  214. value,
  215. fields: schema,
  216. targetProvider: endpoint && endpoint.type,
  217. });
  218. };
  219. let properties: any[] = [];
  220. let dictionaryKey = "";
  221. if (endpoint) {
  222. dictionaryKey = `${endpoint.type}-${type}`;
  223. }
  224. const environment =
  225. this.props.item &&
  226. (type === "source"
  227. ? this.props.item.source_environment
  228. : this.props.item.destination_environment);
  229. propertyNames.forEach(pn => {
  230. const value = environment ? environment[pn] : "";
  231. const label = LabelDictionary.get(pn, dictionaryKey);
  232. if (value && value.join) {
  233. value.forEach((v: any, i: number) => {
  234. const useLabel = i === 0 ? label : "";
  235. properties.push({ label: useLabel, value: v });
  236. });
  237. } else if (value && typeof value === "object") {
  238. properties = properties.concat(
  239. Object.keys(value).map(p => {
  240. if (p === "disk_mappings" || p === "backend_mappings") {
  241. return null;
  242. }
  243. if (value[p] == null || value[p] == undefined) {
  244. return null;
  245. }
  246. return {
  247. label: `${label} - ${LabelDictionary.get(p)}`,
  248. value: getValue(p, value[p]),
  249. };
  250. }),
  251. );
  252. } else {
  253. properties.push({ label, value: getValue(pn, value) });
  254. }
  255. });
  256. return (
  257. <PropertiesTable>
  258. {properties
  259. .filter(Boolean)
  260. .filter(p => p.value != null && p.value !== "")
  261. .map(prop => (
  262. <PropertyRow key={prop.label}>
  263. <PropertyName>{prop.label}</PropertyName>
  264. <PropertyValue>
  265. {prop.label.toLowerCase().indexOf("password") > -1 &&
  266. !this.state.showPassword.find(
  267. f => f === `${prop.label}-${type}`,
  268. ) ? (
  269. <PasswordValue
  270. value={prop.value}
  271. onShow={() =>
  272. this.setState(prevState => ({
  273. showPassword: [
  274. ...prevState.showPassword,
  275. `${prop.label}-${type}`,
  276. ],
  277. }))
  278. }
  279. />
  280. ) : (
  281. <CopyValue value={prop.value} />
  282. )}
  283. </PropertyValue>
  284. </PropertyRow>
  285. ))}
  286. </PropertiesTable>
  287. );
  288. }
  289. renderTable() {
  290. if (this.props.loading) {
  291. return null;
  292. }
  293. const sourceEndpoint = this.getSourceEndpoint();
  294. const destinationEndpoint = this.getDestinationEndpoint();
  295. const lastUpdated = this.renderLastExecutionTime();
  296. const getPropertyNames = (type: "source" | "destination") => {
  297. const env =
  298. this.props.item &&
  299. (type === "source"
  300. ? this.props.item.source_environment
  301. : this.props.item.destination_environment);
  302. return env
  303. ? Object.keys(env).filter(
  304. k =>
  305. k !== "network_map" &&
  306. (k !== "storage_mappings" ||
  307. (env[k] != null &&
  308. typeof env[k] === "object" &&
  309. Object.keys(env[k]).length > 0)),
  310. )
  311. : [];
  312. };
  313. const sourceMinionPool = this.props.minionPools.find(
  314. m => m.id === this.props.item?.origin_minion_pool_id,
  315. );
  316. const destMinionPool = this.props.minionPools.find(
  317. m => m.id === this.props.item?.destination_minion_pool_id,
  318. );
  319. return (
  320. <ColumnsLayout>
  321. <Column width="42.5%">
  322. <Row>
  323. <Field>
  324. <Label>Source</Label>
  325. {this.renderEndpointLink("source")}
  326. </Field>
  327. </Row>
  328. <Row>
  329. <EndpointLogos
  330. endpoint={(sourceEndpoint ? sourceEndpoint.type : "") as any}
  331. />
  332. </Row>
  333. {getPropertyNames("source").length > 0 ? (
  334. <Row>
  335. <Field>
  336. <Label>
  337. Properties
  338. {this.props.sourceSchemaLoading ? (
  339. <StatusIcon
  340. status="RUNNING"
  341. style={{ marginLeft: "8px" }}
  342. />
  343. ) : (
  344. <StatusIconStub />
  345. )}
  346. </Label>
  347. <Value block>
  348. {this.renderPropertiesTable(
  349. getPropertyNames("source"),
  350. "source",
  351. )}
  352. </Value>
  353. </Field>
  354. </Row>
  355. ) : null}
  356. <Row>
  357. <Field>
  358. <Label>Id</Label>
  359. {this.renderValue(
  360. this.props.item ? this.props.item.id || "-" : "-",
  361. )}
  362. </Field>
  363. </Row>
  364. <Row>
  365. <Field>
  366. <Label>Created</Label>
  367. {this.props.item && this.props.item.created_at ? (
  368. this.renderValue(
  369. DateUtils.getLocalDate(this.props.item.created_at).toFormat(
  370. "yyyy-LL-dd HH:mm:ss",
  371. ),
  372. )
  373. ) : (
  374. <Value>-</Value>
  375. )}
  376. </Field>
  377. </Row>
  378. {lastUpdated ? (
  379. <Row>
  380. <Field>
  381. <Label>Last Updated</Label>
  382. <Value>{lastUpdated}</Value>
  383. </Field>
  384. </Row>
  385. ) : null}
  386. {this.props.item?.origin_minion_pool_id ? (
  387. <Row>
  388. <Field>
  389. <Label>Source Minion Pool</Label>
  390. {sourceMinionPool ? (
  391. <ValueLink to={`/minion-pools/${sourceMinionPool.id}`}>
  392. {sourceMinionPool.name}
  393. </ValueLink>
  394. ) : (
  395. <Value>{this.props.item.origin_minion_pool_id}</Value>
  396. )}
  397. </Field>
  398. </Row>
  399. ) : null}
  400. {this.props.item?.type === "deployment" &&
  401. this.props.item.transfer_id ? (
  402. <Row>
  403. <Field>
  404. <Label>Created from Transfer</Label>
  405. <ValueLink to={`/transfers/${this.props.item.transfer_id}`}>
  406. {this.props.item.transfer_id}
  407. </ValueLink>
  408. </Field>
  409. </Row>
  410. ) : null}
  411. </Column>
  412. <Column width="9.5%">
  413. <Arrow />
  414. </Column>
  415. <Column width="48%" style={{ flexGrow: 1 }}>
  416. <Row>
  417. <Field>
  418. <Label>Target</Label>
  419. {this.renderEndpointLink("target")}
  420. </Field>
  421. </Row>
  422. <Row>
  423. <EndpointLogos
  424. endpoint={
  425. (destinationEndpoint ? destinationEndpoint.type : "") as any
  426. }
  427. />
  428. </Row>
  429. {getPropertyNames("destination").length > 0 ? (
  430. <Row>
  431. <Field>
  432. <Label>
  433. Properties
  434. {this.props.destinationSchemaLoading ? (
  435. <StatusIcon
  436. status="RUNNING"
  437. style={{ marginLeft: "8px" }}
  438. />
  439. ) : (
  440. <StatusIconStub />
  441. )}
  442. </Label>
  443. <Value block>
  444. {this.renderPropertiesTable(
  445. getPropertyNames("destination"),
  446. "destination",
  447. )}
  448. </Value>
  449. </Field>
  450. </Row>
  451. ) : null}
  452. {this.props.item?.destination_minion_pool_id ? (
  453. <Row>
  454. <Field>
  455. <Label>Target Minion Pool</Label>
  456. {destMinionPool ? (
  457. <ValueLink to={`/minion-pools/${destMinionPool.id}`}>
  458. {destMinionPool.name}
  459. </ValueLink>
  460. ) : (
  461. <Value>{this.props.item.destination_minion_pool_id}</Value>
  462. )}
  463. </Field>
  464. </Row>
  465. ) : null}
  466. </Column>
  467. </ColumnsLayout>
  468. );
  469. }
  470. renderBottomControls() {
  471. if (this.props.loading) {
  472. return null;
  473. }
  474. return this.props.bottomControls;
  475. }
  476. renderLoading() {
  477. if (!this.props.loading && !this.props.instancesDetailsLoading) {
  478. return null;
  479. }
  480. return (
  481. <Loading>
  482. <StatusImage loading />
  483. </Loading>
  484. );
  485. }
  486. renderSpecialError() {
  487. if (this.props.item?.last_execution_status !== "ERROR_ALLOCATING_MINIONS") {
  488. return null;
  489. }
  490. return (
  491. <WarningWrapper>
  492. <StatusIcon status="ERROR" />
  493. <WarningText>
  494. There was an error allocating minion machines for this{" "}
  495. {this.props.item.type}. Please review the log events for the selected
  496. minion pool(s) and the logs of the Coriolis Minion Manager component
  497. for full details.
  498. </WarningText>
  499. </WarningWrapper>
  500. );
  501. }
  502. render() {
  503. return (
  504. <Wrapper>
  505. {this.renderSpecialError()}
  506. {this.renderTable()}
  507. {this.props.instancesDetailsLoading || this.props.loading ? null : (
  508. <TransferDetailsTable
  509. item={this.props.item}
  510. minionPools={this.props.minionPools}
  511. instancesDetails={this.props.instancesDetails}
  512. networks={this.props.networks}
  513. storageBackends={this.props.storageBackends}
  514. />
  515. )}
  516. {this.renderLoading()}
  517. {this.renderBottomControls()}
  518. </Wrapper>
  519. );
  520. }
  521. }
  522. export default MainDetails;