WizardInstances.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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. // @flow
  15. import React from 'react'
  16. import { observer } from 'mobx-react'
  17. import styled from 'styled-components'
  18. import Checkbox from '../../atoms/Checkbox'
  19. import ReloadButton from '../../atoms/ReloadButton'
  20. import StatusImage from '../../atoms/StatusImage'
  21. import Button from '../../atoms/Button'
  22. import SearchInput from '../../molecules/SearchInput'
  23. import InfoIcon from '../../atoms/InfoIcon'
  24. import Pagination from '../../atoms/Pagination'
  25. import Palette from '../../styleUtils/Palette'
  26. import StyleProps from '../../styleUtils/StyleProps'
  27. import type { Instance as InstanceType } from '../../../types/Instance'
  28. import instanceImage from './images/instance.svg'
  29. import bigInstanceImage from './images/instance-big.svg'
  30. const Wrapper = styled.div`
  31. width: 100%;
  32. display: flex;
  33. flex-direction: column;
  34. `
  35. const LoadingWrapper = styled.div`
  36. margin-top: 32px;
  37. display: flex;
  38. flex-direction: column;
  39. align-items: center;
  40. `
  41. const InstancesWrapper = styled.div`
  42. margin-left: -32px;
  43. flex-grow: 1;
  44. overflow: auto;
  45. `
  46. const InstanceContent = styled.div`
  47. display: flex;
  48. align-items: center;
  49. width: 100%;
  50. padding: 8px 16px;
  51. margin-left: 16px;
  52. border-top: 1px solid ${Palette.grayscale[1]};
  53. transition: background ${StyleProps.animations.swift};
  54. &:hover {
  55. background: ${Palette.grayscale[1]};
  56. }
  57. `
  58. const CheckboxStyled = styled(Checkbox)`
  59. opacity: 0;
  60. transition: all ${StyleProps.animations.swift};
  61. `
  62. const Instance = styled.div`
  63. display: flex;
  64. align-items: center;
  65. position: relative;
  66. cursor: pointer;
  67. ${CheckboxStyled} {
  68. ${props => props.selected ? 'opacity: 1;' : ''}
  69. }
  70. &:hover ${CheckboxStyled} {
  71. opacity: 1;
  72. }
  73. &:last-child ${InstanceContent} {
  74. border-bottom: 1px solid ${Palette.grayscale[1]};
  75. }
  76. `
  77. const LoadingText = styled.div`
  78. margin-top: 38px;
  79. font-size: 18px;
  80. `
  81. export const Image = styled.div`
  82. ${StyleProps.exactSize('48px')}
  83. background: url('${instanceImage}') center no-repeat;
  84. `
  85. const Label = styled.div`
  86. flex-grow: 1;
  87. white-space: nowrap;
  88. text-overflow: ellipsis;
  89. overflow: hidden;
  90. margin: 0 16px;
  91. `
  92. const Details = styled.div`
  93. font-size: 14px;
  94. color: ${Palette.grayscale[4]};
  95. `
  96. const FiltersWrapper = styled.div`
  97. padding: 8px 0 0 8px;
  98. min-height: 24px;
  99. margin-bottom: 16px;
  100. display: flex;
  101. justify-content: space-between;
  102. `
  103. const SearchInputInfo = styled.div`
  104. display: flex;
  105. align-items: center;
  106. `
  107. const FilterInfo = styled.div`
  108. display: flex;
  109. color: ${Palette.grayscale[4]};
  110. `
  111. const SelectionInfo = styled.div``
  112. const FilterSeparator = styled.div`
  113. margin: 0 14px 0 16px;
  114. `
  115. const Reloading = styled.div`
  116. margin: 32px auto 0 auto;
  117. flex-grow: 1;
  118. `
  119. const SearchNotFound = styled.div`
  120. display: flex;
  121. flex-direction: column;
  122. align-items: center;
  123. ${props => props.marginTop ? 'margin-top: 64px;' : ''}
  124. > * {
  125. margin-bottom: 42px;
  126. }
  127. `
  128. const SearchNotFoundText = styled.div`
  129. font-size: 18px;
  130. `
  131. const SearchNotFoundSubtitle = styled.div`
  132. color: ${Palette.grayscale[4]};
  133. margin-top: -32px;
  134. text-align: center;
  135. `
  136. const BigInstanceImage = styled.div`
  137. ${StyleProps.exactSize('96px')}
  138. background: url('${bigInstanceImage}') center no-repeat;
  139. `
  140. type Props = {
  141. instances: InstanceType[],
  142. selectedInstances: ?InstanceType[],
  143. currentPage: number,
  144. instancesPerPage: number,
  145. loading: boolean,
  146. chunksLoading: boolean,
  147. searching: boolean,
  148. searchNotFound: boolean,
  149. reloading: boolean,
  150. hasSourceOptions: boolean,
  151. onSearchInputChange: (value: string) => void,
  152. onReloadClick: () => void,
  153. onInstanceClick: (instance: InstanceType) => void,
  154. onPageClick: (page: number) => void,
  155. }
  156. type State = {
  157. searchText: string,
  158. }
  159. @observer
  160. class WizardInstances extends React.Component<Props, State> {
  161. state = {
  162. searchText: '',
  163. }
  164. timeout: TimeoutID
  165. componentWillUnmount() {
  166. this.props.onSearchInputChange('')
  167. }
  168. handleSeachInputChange(searchText: string) {
  169. clearTimeout(this.timeout)
  170. this.setState({ searchText })
  171. this.timeout = setTimeout(() => {
  172. this.props.onSearchInputChange(searchText)
  173. }, 500)
  174. }
  175. handlePreviousPageClick() {
  176. this.props.onPageClick(this.props.currentPage - 1)
  177. }
  178. handleNextPageClick() {
  179. this.props.onPageClick(this.props.currentPage + 1)
  180. }
  181. areNoInstances() {
  182. return !this.props.loading && !this.props.searchNotFound && !this.props.reloading
  183. && this.props.instances.length === 0 && !this.props.searching
  184. }
  185. renderNoInstances() {
  186. if (!this.areNoInstances()) {
  187. return null
  188. }
  189. let subtitle
  190. if (this.props.hasSourceOptions) {
  191. subtitle = (
  192. <SearchNotFoundSubtitle>
  193. Some platforms require pre-inputting parameters like location or resource containers for listing instances.
  194. <br />Please check that all of the options from the previous screen are correct.
  195. </SearchNotFoundSubtitle>
  196. )
  197. } else {
  198. subtitle = <SearchNotFoundSubtitle>You can retry the search or choose another Endpoint</SearchNotFoundSubtitle>
  199. }
  200. return (
  201. <SearchNotFound marginTop>
  202. <BigInstanceImage />
  203. <SearchNotFoundText>It seems like you don’t have any Instances in this Endpoint</SearchNotFoundText>
  204. {subtitle}
  205. <Button hollow onClick={() => { this.props.onReloadClick() }}>Retry Search</Button>
  206. </SearchNotFound>
  207. )
  208. }
  209. renderSearchNotFound() {
  210. if (!this.props.searchNotFound) {
  211. return null
  212. }
  213. let subtitle = null
  214. if (this.props.hasSourceOptions) {
  215. subtitle = (
  216. <SearchNotFoundSubtitle>
  217. Some platforms require pre-inputting parameters like location or resource containers for
  218. <br />listing instances. Please check that all of the options from the previous screen are correct.
  219. </SearchNotFoundSubtitle>
  220. )
  221. }
  222. return (
  223. <SearchNotFound>
  224. <StatusImage status="ERROR" />
  225. <SearchNotFoundText data-test-id="wInstances-notFoundText">Your search returned no results</SearchNotFoundText>
  226. {subtitle}
  227. <Button hollow onClick={() => { this.props.onReloadClick() }}>Retry</Button>
  228. </SearchNotFound>
  229. )
  230. }
  231. renderReloading() {
  232. if (!this.props.reloading) {
  233. return null
  234. }
  235. return (
  236. <Reloading>
  237. <StatusImage loading />
  238. </Reloading>
  239. )
  240. }
  241. renderLoading() {
  242. if (!this.props.loading) {
  243. return null
  244. }
  245. return (
  246. <LoadingWrapper>
  247. <StatusImage loading data-test-id="wInstances-loadingStatus" />
  248. <LoadingText>Loading instances...</LoadingText>
  249. </LoadingWrapper>
  250. )
  251. }
  252. renderInstances() {
  253. if (this.props.loading || this.props.searchNotFound || this.props.reloading || this.areNoInstances()) {
  254. return null
  255. }
  256. let startIdx = (this.props.currentPage - 1) * this.props.instancesPerPage
  257. let endIdx = startIdx + (this.props.instancesPerPage - 1)
  258. let filteredInstances = this.props.instances.filter((i, idx) => idx >= startIdx && idx <= endIdx)
  259. return (
  260. <InstancesWrapper>
  261. {filteredInstances.map(instance => {
  262. let selected = Boolean(this.props.selectedInstances && this.props.selectedInstances.find(i => i.id === instance.id))
  263. let flavorName = instance.flavor_name ? ` | ${instance.flavor_name}` : ''
  264. return (
  265. <Instance
  266. key={instance.id}
  267. onClick={() => { this.props.onInstanceClick(instance) }}
  268. selected={selected}
  269. data-test-id={`wInstances-item-${instance.id}`}
  270. >
  271. <CheckboxStyled checked={selected} onChange={() => { }} />
  272. <InstanceContent data-test-id="wInstances-instanceItem">
  273. <Image />
  274. <Label data-test-id="wInstances-itemName">{instance.instance_name || instance.name}</Label>
  275. <Details data-test-id="wInstances-itemDetails">{`${instance.num_cpu} vCPU | ${instance.memory_mb} MB RAM${flavorName}`}</Details>
  276. </InstanceContent>
  277. </Instance>
  278. )
  279. })}
  280. </InstancesWrapper>
  281. )
  282. }
  283. renderFilters() {
  284. if (this.props.loading || this.areNoInstances()) {
  285. return null
  286. }
  287. let count = this.props.selectedInstances ? this.props.selectedInstances.length : 0
  288. let plural = count === 1 ? '' : 's'
  289. return (
  290. <FiltersWrapper>
  291. <SearchInputInfo>
  292. <SearchInput
  293. alwaysOpen
  294. onChange={searchText => { this.handleSeachInputChange(searchText) }}
  295. value={this.state.searchText}
  296. loading={this.props.searching}
  297. placeholder="Search VMs"
  298. data-test-id="wInstances-searchInput"
  299. />
  300. {this.props.hasSourceOptions ? (
  301. <InfoIcon
  302. 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."
  303. marginBottom={0}
  304. marginLeft={8}
  305. filled
  306. />
  307. ) : null}
  308. </SearchInputInfo>
  309. <FilterInfo>
  310. <SelectionInfo data-test-id="wInstances-selInfo">{count} instance{plural} selected</SelectionInfo>
  311. <FilterSeparator>|</FilterSeparator>
  312. <ReloadButton
  313. onClick={() => { this.props.onReloadClick() }}
  314. data-test-id="wInstances-reloadButton"
  315. />
  316. </FilterInfo>
  317. </FiltersWrapper>
  318. )
  319. }
  320. renderPagination() {
  321. if (this.props.loading || this.props.searchNotFound || this.props.reloading || this.areNoInstances()) {
  322. return null
  323. }
  324. let hasNextPage = this.props.currentPage * this.props.instancesPerPage < this.props.instances.length
  325. let areAllDisabled = this.props.searching
  326. let isPreviousDisabled = this.props.currentPage === 1 || areAllDisabled
  327. let isNextDisabled = !hasNextPage || areAllDisabled
  328. return (
  329. <Pagination
  330. style={{ margin: '32px 0 16px 0' }}
  331. previousDisabled={isPreviousDisabled}
  332. onPreviousClick={() => { this.handlePreviousPageClick() }}
  333. currentPage={this.props.currentPage}
  334. totalPages={Math.ceil(this.props.instances.length / this.props.instancesPerPage)}
  335. loading={this.props.chunksLoading}
  336. nextDisabled={isNextDisabled}
  337. onNextClick={() => { this.handleNextPageClick() }}
  338. />
  339. )
  340. }
  341. render() {
  342. return (
  343. <Wrapper>
  344. {this.renderFilters()}
  345. {this.renderLoading()}
  346. {this.renderReloading()}
  347. {this.renderSearchNotFound()}
  348. {this.renderInstances()}
  349. {this.renderPagination()}
  350. {this.renderNoInstances()}
  351. </Wrapper>
  352. )
  353. }
  354. }
  355. export default WizardInstances