WizardInstances.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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 React from "react";
  15. import { observer } from "mobx-react";
  16. import styled from "styled-components";
  17. import Checkbox from "@src/components/ui/Checkbox";
  18. import ReloadButton from "@src/components/ui/ReloadButton";
  19. import StatusImage from "@src/components/ui/StatusComponents/StatusImage";
  20. import Button from "@src/components/ui/Button";
  21. import SearchInput from "@src/components/ui/SearchInput";
  22. import InfoIcon from "@src/components/ui/InfoIcon";
  23. import Pagination from "@src/components/ui/Pagination";
  24. import { ThemePalette, ThemeProps } from "@src/components/Theme";
  25. import type { Instance as InstanceType } from "@src/@types/Instance";
  26. import instanceImage from "./images/instance.svg";
  27. import instanceLinuxImage from "./images/instance-linux.svg";
  28. import instanceWindowsImage from "./images/instance-windows.svg";
  29. import bigInstanceImage from "./images/instance-big.svg";
  30. const mbToGbString = (mb: number) =>
  31. mb >= 1024 ? `${(mb / 1024).toFixed(2)} GB` : `${mb} MB`;
  32. const Wrapper = styled.div<any>`
  33. width: 100%;
  34. display: flex;
  35. flex-direction: column;
  36. `;
  37. const LoadingWrapper = styled.div<any>`
  38. margin-top: 32px;
  39. display: flex;
  40. flex-direction: column;
  41. align-items: center;
  42. `;
  43. const InstancesWrapper = styled.div<any>`
  44. margin-left: -32px;
  45. flex-grow: 1;
  46. overflow: auto;
  47. `;
  48. const InstanceContent = styled.div<any>`
  49. display: flex;
  50. align-items: center;
  51. width: 100%;
  52. padding: 8px 16px;
  53. margin-left: 16px;
  54. border-top: 1px solid ${ThemePalette.grayscale[1]};
  55. transition: background ${ThemeProps.animations.swift};
  56. &:hover {
  57. background: ${ThemePalette.grayscale[1]};
  58. }
  59. `;
  60. const CheckboxStyled = styled(Checkbox)`
  61. opacity: 0;
  62. transition: all ${ThemeProps.animations.swift};
  63. :focus {
  64. opacity: 1;
  65. }
  66. `;
  67. const Instance = styled.div<any>`
  68. display: flex;
  69. align-items: center;
  70. position: relative;
  71. cursor: pointer;
  72. ${CheckboxStyled} {
  73. ${props => (props.selected ? "opacity: 1;" : "")}
  74. }
  75. &:hover ${CheckboxStyled} {
  76. opacity: 1;
  77. }
  78. &:last-child ${InstanceContent} {
  79. border-bottom: 1px solid ${ThemePalette.grayscale[1]};
  80. }
  81. `;
  82. const LoadingText = styled.div<any>`
  83. margin-top: 38px;
  84. font-size: 18px;
  85. `;
  86. const getOsImage = (osType?: string) => {
  87. if (osType === "linux") {
  88. return instanceLinuxImage;
  89. }
  90. if (osType === "windows") {
  91. return instanceWindowsImage;
  92. }
  93. return instanceImage;
  94. };
  95. export const InstanceImage = styled.div<{ os?: string }>`
  96. ${ThemeProps.exactSize("48px")}
  97. background: url('${props => getOsImage(props.os)}') center no-repeat;
  98. `;
  99. const Label = styled.div<any>`
  100. flex-grow: 1;
  101. margin: 0 16px;
  102. display: flex;
  103. flex-direction: column;
  104. `;
  105. const LabelTitle = styled.div``;
  106. const LabelSubtitle = styled.div`
  107. color: ${ThemePalette.grayscale[4]};
  108. overflow-wrap: anywhere;
  109. `;
  110. const Details = styled.div<any>`
  111. color: ${ThemePalette.grayscale[4]};
  112. min-width: 160px;
  113. text-align: right;
  114. `;
  115. const FiltersWrapper = styled.div<any>`
  116. padding: 8px 0 0 8px;
  117. min-height: 24px;
  118. margin-bottom: 16px;
  119. display: flex;
  120. justify-content: space-between;
  121. `;
  122. const SearchInputInfo = styled.div<any>`
  123. display: flex;
  124. align-items: center;
  125. `;
  126. const FilterInfo = styled.div<any>`
  127. display: flex;
  128. color: ${ThemePalette.grayscale[4]};
  129. `;
  130. const SelectionInfo = styled.div<any>``;
  131. const FilterSeparator = styled.div<any>`
  132. margin: 0 14px 0 16px;
  133. `;
  134. const Reloading = styled.div<any>`
  135. margin: 32px auto 0 auto;
  136. flex-grow: 1;
  137. `;
  138. const SearchNotFound = styled.div<any>`
  139. display: flex;
  140. flex-direction: column;
  141. align-items: center;
  142. ${props => (props.marginTop ? "margin-top: 64px;" : "")}
  143. > * {
  144. margin-bottom: 42px;
  145. }
  146. `;
  147. const SearchNotFoundText = styled.div<any>`
  148. font-size: 18px;
  149. `;
  150. const SearchNotFoundSubtitle = styled.div<any>`
  151. color: ${ThemePalette.grayscale[4]};
  152. margin-top: -32px;
  153. text-align: center;
  154. `;
  155. const BigInstanceImage = styled.div<any>`
  156. ${ThemeProps.exactSize("96px")}
  157. background: url('${bigInstanceImage}') center no-repeat;
  158. `;
  159. type Props = {
  160. instances: InstanceType[];
  161. selectedInstances?: InstanceType[] | null;
  162. currentPage: number;
  163. instancesPerPage: number;
  164. loading: boolean;
  165. chunksLoading: boolean;
  166. searching: boolean;
  167. searchNotFound: boolean;
  168. reloading: boolean;
  169. hasSourceOptions: boolean;
  170. searchText?: string;
  171. onSearchInputChange: (value: string) => void;
  172. onReloadClick: () => void;
  173. onInstanceClick: (instance: InstanceType) => void;
  174. onPageClick: (page: number) => void;
  175. };
  176. type State = {
  177. searchText: string;
  178. };
  179. @observer
  180. class WizardInstances extends React.Component<Props, State> {
  181. state = {
  182. searchText: "",
  183. };
  184. timeout!: number;
  185. isCheckboxMouseDown = false;
  186. componentWillUnmount() {
  187. this.props.onSearchInputChange("");
  188. }
  189. handleSeachInputChange(searchText: string) {
  190. clearTimeout(this.timeout);
  191. this.setState({ searchText });
  192. this.timeout = window.setTimeout(() => {
  193. this.props.onSearchInputChange(searchText);
  194. }, 500);
  195. }
  196. handlePreviousPageClick() {
  197. this.props.onPageClick(this.props.currentPage - 1);
  198. }
  199. handleNextPageClick() {
  200. this.props.onPageClick(this.props.currentPage + 1);
  201. }
  202. areNoInstances() {
  203. return (
  204. !this.props.loading &&
  205. !this.props.searchNotFound &&
  206. !this.props.reloading &&
  207. this.props.instances.length === 0 &&
  208. !this.props.searching
  209. );
  210. }
  211. renderNoInstances() {
  212. if (!this.areNoInstances()) {
  213. return null;
  214. }
  215. let subtitle;
  216. if (this.props.hasSourceOptions) {
  217. subtitle = (
  218. <SearchNotFoundSubtitle>
  219. Some platforms require pre-inputting parameters like location or
  220. resource containers for listing instances.
  221. <br />
  222. Please check that all of the options from the previous screen are
  223. correct.
  224. </SearchNotFoundSubtitle>
  225. );
  226. } else {
  227. subtitle = (
  228. <SearchNotFoundSubtitle>
  229. You can retry the search or choose another Endpoint
  230. </SearchNotFoundSubtitle>
  231. );
  232. }
  233. return (
  234. <SearchNotFound marginTop>
  235. <BigInstanceImage />
  236. <SearchNotFoundText>
  237. It seems like you don’t have any Instances in this Endpoint
  238. </SearchNotFoundText>
  239. {subtitle}
  240. <Button
  241. hollow
  242. onClick={() => {
  243. this.props.onReloadClick();
  244. }}
  245. >
  246. Retry Search
  247. </Button>
  248. </SearchNotFound>
  249. );
  250. }
  251. renderSearchNotFound() {
  252. if (!this.props.searchNotFound) {
  253. return null;
  254. }
  255. let subtitle = null;
  256. if (this.props.hasSourceOptions) {
  257. subtitle = (
  258. <SearchNotFoundSubtitle>
  259. Some platforms require pre-inputting parameters like location or
  260. resource containers for
  261. <br />
  262. listing instances. Please check that all of the options from the
  263. previous screen are correct.
  264. </SearchNotFoundSubtitle>
  265. );
  266. }
  267. return (
  268. <SearchNotFound>
  269. <StatusImage status="ERROR" />
  270. <SearchNotFoundText>Your search returned no results</SearchNotFoundText>
  271. {subtitle}
  272. <Button
  273. hollow
  274. onClick={() => {
  275. this.props.onReloadClick();
  276. }}
  277. >
  278. Retry
  279. </Button>
  280. </SearchNotFound>
  281. );
  282. }
  283. renderReloading() {
  284. if (!this.props.reloading) {
  285. return null;
  286. }
  287. return (
  288. <Reloading>
  289. <StatusImage loading />
  290. </Reloading>
  291. );
  292. }
  293. renderLoading() {
  294. if (!this.props.loading) {
  295. return null;
  296. }
  297. return (
  298. <LoadingWrapper>
  299. <StatusImage loading />
  300. <LoadingText>Loading instances...</LoadingText>
  301. </LoadingWrapper>
  302. );
  303. }
  304. renderInstances() {
  305. if (
  306. this.props.loading ||
  307. this.props.searchNotFound ||
  308. this.props.reloading ||
  309. this.areNoInstances()
  310. ) {
  311. return null;
  312. }
  313. const startIdx = (this.props.currentPage - 1) * this.props.instancesPerPage;
  314. const endIdx = startIdx + (this.props.instancesPerPage - 1);
  315. const filteredInstances = this.props.instances.filter(
  316. (_, idx) => idx >= startIdx && idx <= endIdx
  317. );
  318. return (
  319. <InstancesWrapper>
  320. {filteredInstances.map(instance => {
  321. const selected = Boolean(
  322. this.props.selectedInstances &&
  323. this.props.selectedInstances.find(i => i.id === instance.id)
  324. );
  325. const flavorName = instance.flavor_name
  326. ? ` | ${instance.flavor_name}`
  327. : "";
  328. const instanceId = instance.instance_name || instance.id;
  329. return (
  330. <Instance
  331. key={instance.id}
  332. onMouseDown={() => {
  333. if (!this.isCheckboxMouseDown)
  334. this.props.onInstanceClick(instance);
  335. }}
  336. selected={selected}
  337. >
  338. <CheckboxStyled
  339. checked={selected}
  340. onChange={() => {
  341. this.props.onInstanceClick(instance);
  342. }}
  343. onMouseDown={() => {
  344. this.isCheckboxMouseDown = true;
  345. }}
  346. onMouseUp={() => {
  347. this.isCheckboxMouseDown = false;
  348. }}
  349. />
  350. <InstanceContent>
  351. <InstanceImage os={instance.os_type} />
  352. <Label>
  353. <LabelTitle>{instance.name}</LabelTitle>
  354. {instanceId !== instance.name ? (
  355. <LabelSubtitle>{instanceId}</LabelSubtitle>
  356. ) : null}
  357. </Label>
  358. <Details>
  359. {`${instance.num_cpu} vCPU | ${mbToGbString(
  360. instance.memory_mb
  361. )} RAM${flavorName}`}
  362. </Details>
  363. </InstanceContent>
  364. </Instance>
  365. );
  366. })}
  367. </InstancesWrapper>
  368. );
  369. }
  370. renderFilters() {
  371. if (this.props.loading || this.areNoInstances()) {
  372. return null;
  373. }
  374. const count = this.props.selectedInstances
  375. ? this.props.selectedInstances.length
  376. : 0;
  377. const plural = count === 1 ? "" : "s";
  378. return (
  379. <FiltersWrapper>
  380. <SearchInputInfo>
  381. <SearchInput
  382. alwaysOpen
  383. onChange={searchText => {
  384. this.handleSeachInputChange(searchText);
  385. }}
  386. value={this.state.searchText}
  387. loading={this.props.searching}
  388. placeholder="Search VMs"
  389. />
  390. {this.props.hasSourceOptions ? (
  391. <InfoIcon
  392. text="Some platforms require pre-inputting parameters like location or resource containers for listing instances. Please check that all of the options from the previous screen are correct."
  393. marginBottom={0}
  394. marginLeft={8}
  395. filled
  396. />
  397. ) : null}
  398. </SearchInputInfo>
  399. <FilterInfo>
  400. <SelectionInfo>
  401. {count} instance{plural} selected
  402. </SelectionInfo>
  403. <FilterSeparator>|</FilterSeparator>
  404. <ReloadButton
  405. onClick={() => {
  406. this.props.onReloadClick();
  407. }}
  408. />
  409. </FilterInfo>
  410. </FiltersWrapper>
  411. );
  412. }
  413. renderPagination() {
  414. if (
  415. this.props.loading ||
  416. this.props.searchNotFound ||
  417. this.props.reloading ||
  418. this.areNoInstances()
  419. ) {
  420. return null;
  421. }
  422. const hasNextPage =
  423. this.props.currentPage * this.props.instancesPerPage <
  424. this.props.instances.length;
  425. const areAllDisabled = this.props.searching;
  426. const isPreviousDisabled = this.props.currentPage === 1 || areAllDisabled;
  427. const isNextDisabled = !hasNextPage || areAllDisabled;
  428. return (
  429. <Pagination
  430. style={{ margin: "32px 0 16px 0" }}
  431. previousDisabled={isPreviousDisabled}
  432. onPreviousClick={() => {
  433. this.handlePreviousPageClick();
  434. }}
  435. currentPage={this.props.currentPage}
  436. totalPages={Math.ceil(
  437. this.props.instances.length / this.props.instancesPerPage
  438. )}
  439. loading={this.props.chunksLoading}
  440. nextDisabled={isNextDisabled}
  441. onNextClick={() => {
  442. this.handleNextPageClick();
  443. }}
  444. />
  445. );
  446. }
  447. render() {
  448. return (
  449. <Wrapper>
  450. {this.renderFilters()}
  451. {this.renderLoading()}
  452. {this.renderReloading()}
  453. {this.renderSearchNotFound()}
  454. {this.renderInstances()}
  455. {this.renderPagination()}
  456. {this.renderNoInstances()}
  457. </Wrapper>
  458. );
  459. }
  460. }
  461. export default WizardInstances;