MetalHubServerDetailsContent.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. /*
  2. Copyright (C) 2022 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 React from "react";
  15. import { observer } from "mobx-react";
  16. import styled from "styled-components";
  17. import moment from "moment";
  18. import CopyValue from "@src/components/ui/CopyValue";
  19. import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
  20. import Button from "@src/components/ui/Button";
  21. import { ThemePalette, ThemeProps } from "@src/components/Theme";
  22. import {
  23. MetalHubDisk,
  24. MetalHubNic,
  25. MetalHubServer,
  26. } from "@src/@types/MetalHub";
  27. import {
  28. ArrowStyled,
  29. GlobalStyle,
  30. HeaderIcon,
  31. HeaderName,
  32. Row,
  33. RowBody,
  34. RowBodyColumn,
  35. RowBodyColumnValue,
  36. RowHeader,
  37. RowHeaderColumn,
  38. } from "@src/components/modules/TransferModule/TransferDetailsTable";
  39. import { Collapse } from "react-collapse";
  40. import LoadingButton from "@src/components/ui/LoadingButton";
  41. import StatusPill from "@src/components/ui/StatusComponents/StatusPill";
  42. const Wrapper = styled.div`
  43. ${ThemeProps.exactWidth(ThemeProps.contentWidth)}
  44. margin: 0 auto;
  45. padding-left: 126px;
  46. `;
  47. const Info = styled.div`
  48. display: flex;
  49. flex-wrap: wrap;
  50. margin-top: 32px;
  51. margin-left: -32px;
  52. `;
  53. const Field = styled.div`
  54. ${ThemeProps.exactWidth("calc(50% - 32px)")}
  55. margin-bottom: 32px;
  56. margin-left: 32px;
  57. `;
  58. const Value = styled.div``;
  59. const Label = styled.div`
  60. font-size: 10px;
  61. font-weight: ${ThemeProps.fontWeights.medium};
  62. color: ${ThemePalette.grayscale[3]};
  63. text-transform: uppercase;
  64. margin-bottom: 3px;
  65. `;
  66. const LoadingWrapper = styled.div`
  67. display: flex;
  68. justify-content: center;
  69. width: 100%;
  70. margin: 32px 0 64px 0;
  71. `;
  72. const Buttons = styled.div<any>`
  73. margin-top: 64px;
  74. display: flex;
  75. justify-content: space-between;
  76. `;
  77. const ButtonsColumn = styled.div<any>`
  78. display: flex;
  79. flex-direction: column;
  80. button {
  81. margin-bottom: 16px;
  82. &:last-child {
  83. margin-bottom: 0;
  84. }
  85. }
  86. `;
  87. const Table = styled.div``;
  88. const TableBody = styled.div``;
  89. const TableHeader = styled.div`
  90. background: ${ThemePalette.grayscale[1]};
  91. border-radius: ${ThemeProps.borderRadius};
  92. margin-bottom: 32px;
  93. &:last-child {
  94. margin-bottom: 0;
  95. }
  96. `;
  97. const TableHeaderInfo = styled.div`
  98. padding: 16px;
  99. border-bottom: 1px solid ${ThemePalette.grayscale[5]};
  100. font-size: 16px;
  101. `;
  102. const TableBodyContent = styled.div`
  103. font-size: 14px;
  104. `;
  105. const HeaderSubtitle = styled.span`
  106. color: ${ThemePalette.grayscale[5]};
  107. margin-left: 4px;
  108. `;
  109. type Props = {
  110. server: MetalHubServer | null;
  111. loading: boolean;
  112. creatingReplica: boolean;
  113. creatingMigration: boolean;
  114. onCreateReplicaClick: () => void;
  115. onCreateMigrationClick: () => void;
  116. onDeleteClick: () => void;
  117. };
  118. type State = {
  119. openedRows: string[];
  120. };
  121. @observer
  122. class MetalHubServerDetailsContent extends React.Component<Props, State> {
  123. state: State = {
  124. openedRows: [],
  125. };
  126. handleRowClick(id: string) {
  127. if (this.state.openedRows.find(i => i === id)) {
  128. this.setState(prevState => ({
  129. openedRows: prevState.openedRows.filter(i => id !== i),
  130. }));
  131. } else {
  132. this.setState(prevState => ({
  133. openedRows: [...prevState.openedRows, id],
  134. }));
  135. }
  136. }
  137. renderLoading() {
  138. return (
  139. <LoadingWrapper>
  140. <StatusImage />
  141. </LoadingWrapper>
  142. );
  143. }
  144. renderButtons() {
  145. if (this.props.loading) {
  146. return null;
  147. }
  148. const creating = this.props.creatingReplica || this.props.creatingMigration;
  149. return (
  150. <Buttons>
  151. <ButtonsColumn>
  152. {this.props.creatingReplica ? (
  153. <LoadingButton>Loading Wizard ...</LoadingButton>
  154. ) : (
  155. <Button
  156. onClick={this.props.onCreateReplicaClick}
  157. disabled={creating || !this.props.server?.active}
  158. hollow
  159. >
  160. Create Replica
  161. </Button>
  162. )}
  163. {this.props.creatingMigration ? (
  164. <LoadingButton>Loading Wizard ...</LoadingButton>
  165. ) : (
  166. <Button
  167. hollow
  168. disabled={creating || !this.props.server?.active}
  169. onClick={this.props.onCreateMigrationClick}
  170. >
  171. Create Migration
  172. </Button>
  173. )}
  174. </ButtonsColumn>
  175. <ButtonsColumn>
  176. <Button
  177. alert
  178. hollow
  179. onClick={() => {
  180. this.props.onDeleteClick();
  181. }}
  182. >
  183. Remove Server
  184. </Button>
  185. </ButtonsColumn>
  186. </Buttons>
  187. );
  188. }
  189. renderInfo() {
  190. if (this.props.loading || !this.props.server) {
  191. return null;
  192. }
  193. const server = this.props.server;
  194. return (
  195. <Info>
  196. <Field>
  197. <Label>Hostname</Label>
  198. {this.renderValue(server.hostname || "-")}
  199. </Field>
  200. <Field>
  201. <Label>ID</Label>
  202. {this.renderValue(String(server.id))}
  203. </Field>
  204. <Field>
  205. <Label>Status</Label>
  206. <Value>
  207. {server.active ? (
  208. <StatusPill status="COMPLETED" label="Active" />
  209. ) : (
  210. <StatusPill status="ERROR" label="Inactive" />
  211. )}
  212. </Value>
  213. </Field>
  214. <Field>
  215. <Label>API Endpoint</Label>
  216. {this.renderValue(String(server.api_endpoint))}
  217. </Field>
  218. <Field>
  219. <Label>Created At</Label>
  220. <Value>
  221. {moment(server.created_at).format("YYYY-MM-DD HH:mm:ss")}
  222. </Value>
  223. </Field>
  224. <Field>
  225. <Label>Updated At</Label>
  226. <Value>
  227. {moment(server.updated_at).format("YYYY-MM-DD HH:mm:ss")}
  228. </Value>
  229. </Field>
  230. <Field>
  231. <Label>Firmware Type</Label>
  232. {this.renderValue(server.firmware_type || "-")}
  233. </Field>
  234. <Field>
  235. <Label>CPU Cores</Label>
  236. {server.physical_cores || "-"} physical, {server.logical_cores || "-"}{" "}
  237. logical
  238. </Field>
  239. <Field>
  240. <Label>Memory Size</Label>
  241. {server.memory
  242. ? this.renderValue(
  243. `${(server.memory / 1024 / 1024 / 1024).toFixed(2)} GB`
  244. )
  245. : "-"}
  246. </Field>
  247. <Field>
  248. <Label>Operating System</Label>
  249. {server.os_info.os_name
  250. ? this.renderValue(
  251. `${server.os_info.os_name} ${server.os_info.os_version}`
  252. )
  253. : "-"}
  254. </Field>
  255. </Info>
  256. );
  257. }
  258. renderValue(value: string) {
  259. return value !== "-" ? (
  260. <CopyValue value={value} maxWidth="90%" />
  261. ) : (
  262. <Value>{value}</Value>
  263. );
  264. }
  265. renderNics(nics: MetalHubNic[]) {
  266. return nics.map(nic => {
  267. const isOpened = Boolean(
  268. this.state.openedRows.find(i => i === nic.nic_name)
  269. );
  270. return (
  271. <Row
  272. key={nic.nic_name}
  273. onClick={() => {
  274. this.handleRowClick(nic.nic_name);
  275. }}
  276. >
  277. <ArrowStyled
  278. primary
  279. orientation={isOpened ? "up" : "down"}
  280. opacity={isOpened ? 1 : 0}
  281. thick
  282. />
  283. <RowHeader>
  284. <RowHeaderColumn>
  285. <HeaderIcon icon="network" />
  286. <HeaderName source>{nic.nic_name}</HeaderName>
  287. </RowHeaderColumn>
  288. <RowHeaderColumn />
  289. </RowHeader>
  290. <Collapse isOpened={isOpened}>
  291. <RowBody>
  292. <RowBodyColumn>
  293. {[
  294. `MAC Address: ${nic.mac_address}`,
  295. `IP Addresses: ${nic.ip_addresses.join(", ")}`,
  296. `Interface Type: ${nic.interface_type}`,
  297. ].map(l => (
  298. <RowBodyColumnValue key={l}>{l}</RowBodyColumnValue>
  299. ))}
  300. </RowBodyColumn>
  301. </RowBody>
  302. </Collapse>
  303. </Row>
  304. );
  305. });
  306. }
  307. renderPartitions(
  308. partitions: MetalHubDisk["partitions"] = [],
  309. sectorSize = 0
  310. ) {
  311. return partitions.map(partition => {
  312. if (!partition.partition_uuid) {
  313. return null;
  314. }
  315. const isOpened = Boolean(
  316. this.state.openedRows.find(i => i === partition.partition_uuid)
  317. );
  318. const size = partition.sectors * sectorSize;
  319. const sizeString = sectorSize
  320. ? size < 1024 * 1024 * 1024
  321. ? `${(size / 1024 / 1024).toFixed(2)} MB`
  322. : `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`
  323. : "Size unavailable";
  324. return (
  325. <Row
  326. key={partition.partition_uuid}
  327. onClick={() => {
  328. this.handleRowClick(partition.partition_uuid!);
  329. }}
  330. >
  331. <ArrowStyled
  332. primary
  333. orientation={isOpened ? "up" : "down"}
  334. opacity={isOpened ? 1 : 0}
  335. thick
  336. />
  337. <RowHeader>
  338. <RowHeaderColumn>
  339. <HeaderIcon icon="storage" />
  340. <HeaderName source>
  341. {partition.name} <HeaderSubtitle>{sizeString}</HeaderSubtitle>
  342. </HeaderName>
  343. </RowHeaderColumn>
  344. <RowHeaderColumn />
  345. </RowHeader>
  346. <Collapse isOpened={isOpened}>
  347. <RowBody>
  348. <RowBodyColumn>
  349. {[
  350. `ID: ${partition.partition_uuid}`,
  351. `Path: ${partition.path}`,
  352. `Sectors: ${partition.sectors}`,
  353. `Start Sector: ${partition.start_sector}`,
  354. `End Sector: ${partition.end_sector}`,
  355. ].map(l => (
  356. <RowBodyColumnValue key={l}>{l}</RowBodyColumnValue>
  357. ))}
  358. </RowBodyColumn>
  359. </RowBody>
  360. </Collapse>
  361. </Row>
  362. );
  363. });
  364. }
  365. renderNicsTable() {
  366. if (this.props.loading || !this.props.server?.nics) {
  367. return null;
  368. }
  369. return (
  370. <Table style={{ marginTop: "24px" }}>
  371. <Label>Network Interface Controllers</Label>
  372. <TableBody>
  373. <TableHeader>
  374. <TableBodyContent>
  375. {this.renderNics(this.props.server.nics)}
  376. </TableBodyContent>
  377. </TableHeader>
  378. </TableBody>
  379. </Table>
  380. );
  381. }
  382. renderPartitionsTable() {
  383. if (this.props.loading || !this.props.server?.disks) {
  384. return null;
  385. }
  386. return (
  387. <Table>
  388. <GlobalStyle />
  389. <Label>Disk Partitions</Label>
  390. <TableBody>
  391. {this.props.server.disks.map(disk => (
  392. <TableHeader key={disk.id}>
  393. <TableHeaderInfo>
  394. {disk.name}{" "}
  395. {disk.size ? (
  396. <HeaderSubtitle>
  397. {(disk.size / 1024 / 1024 / 1024).toFixed(2)} GB
  398. </HeaderSubtitle>
  399. ) : null}
  400. </TableHeaderInfo>
  401. <TableBodyContent>
  402. {this.renderPartitions(
  403. disk.partitions,
  404. disk.physical_sector_size
  405. )}
  406. </TableBodyContent>
  407. </TableHeader>
  408. ))}
  409. </TableBody>
  410. </Table>
  411. );
  412. }
  413. render() {
  414. return (
  415. <Wrapper>
  416. {this.renderInfo()}
  417. {this.renderPartitionsTable()}
  418. {this.renderNicsTable()}
  419. {this.props.loading ? this.renderLoading() : null}
  420. {this.renderButtons()}
  421. <GlobalStyle />
  422. </Wrapper>
  423. );
  424. }
  425. }
  426. export default MetalHubServerDetailsContent;